Compare commits

..

23 Commits

Author SHA1 Message Date
Lea Anthony
4a1a4d75ad [v2] v2.0.0-alpha.73 2021-07-16 22:31:53 +10:00
Lea Anthony
b552c16539 [windows] Add key handler for Webview2 to prevent default hotkeys. 2021-07-16 22:30:58 +10:00
Lea Anthony
d574d53fca [windows] Add debug log for Webview2 version and minimum required version 2021-07-16 21:13:34 +10:00
Lea Anthony
2b69ac8391 [windows] Update manifest to use windows common comtrols v6 2021-07-16 20:15:08 +10:00
Lea Anthony
e7cb40d5ee [windows] use idgen to track menu IDs. Fix chechbox typo 2021-07-11 20:21:23 +10:00
Lea Anthony
f409dbdab1 [v2] v2.0.0-alpha.72 2021-07-11 14:15:05 +10:00
Lea Anthony
1748e8479f [windows] Fixes for window sizing 2021-07-11 11:29:34 +10:00
Lea Anthony
4f2788a294 [windows] Fixes for radiobox sync 2021-07-10 17:03:55 +10:00
Travis McLane
856b81ab04 add missing import 2021-07-09 15:08:59 -05:00
Lea Anthony
76aab2271c [windows] Menu checkboxes and radio groups now fully in sync when using Go menus mutltiple times. 2021-07-08 23:34:56 +10:00
Lea Anthony
3192026e6d [windows] Support updating application menu 2021-07-06 21:07:35 +10:00
Lea Anthony
90dd05e52e [windows] Support radio groups in menus 2021-07-06 07:30:48 +10:00
Lea Anthony
e73cf44ddc [windows] Preliminary support for application menu. More work TBD. 2021-07-05 21:37:48 +10:00
Lea Anthony
4c2804eac9 [v2] Create default windows app menu 2021-07-04 13:59:21 +10:00
Lea Anthony
e1dd77fd3f [v2] Remove SetTitle from templates 2021-07-04 09:51:19 +10:00
Lea Anthony
b69f1e6c43 [v2] Put mac specific calls behind build tag 2021-07-04 09:35:45 +10:00
Lea Anthony
7661082d58 [v2] v2.0.0-alpha.71 2021-07-03 18:35:14 +10:00
Lea Anthony
34da4f056a [mac] Fix onTrayMenuOpen by being less aggressive with freeing memory 😀 2021-07-03 18:34:24 +10:00
Lea Anthony
642c1d5ec5 [v2] Target ARM on Apple Silicon, even if CLI is compiled for AMD64 2021-07-03 16:32:47 +10:00
Lea Anthony
1f3351ffa5 [v2] Get wails dev to compile again 2021-06-30 21:36:30 +10:00
Lea Anthony
30e96118b1 [v2] Remove debug statement 2021-06-30 21:00:09 +10:00
Lea Anthony
efd768ac5d [v2] Remove wails debug (now a build flag) 2021-06-30 20:52:18 +10:00
Lea Anthony
6dbcd4fc45 [v2] New Svelte template. Updates to vanilla. Improved wails generate template 2021-06-30 20:50:39 +10:00
66 changed files with 5800 additions and 308 deletions

View File

@@ -21,6 +21,7 @@ The build command processes the Wails project and generates an application binar
| -upx | Compress final binary with UPX (if installed) | |
| -upxflags "custom flags" | Flags to pass to upx | |
| -v int | Verbosity level (0 - silent, 1 - default, 2 - verbose) | 1 |
| -delve | If true, runs delve on the compiled binary | false |
## The Build Process
@@ -44,11 +45,4 @@ The build process is as follows:
- If the `-upx` flag was provided, `upx` is invoked to compress the binary. Custom flags may be provided using the `-upxflags` flag.
- If the `package` flag is given for a non Windows target, the application is bundled for the platform. On Mac, this creates a `.app` with the processed icons, the `Info.plist` in `build/darwin` and the compiled binary.
### Server Target
TBD
### Hybrid Target
TBD

View File

@@ -4,11 +4,16 @@ import (
"fmt"
"io"
"os"
"os/exec"
"runtime"
"strings"
"text/tabwriter"
"time"
"github.com/wailsapp/wails/v2/internal/system"
"github.com/wailsapp/wails/v2/internal/shell"
"github.com/leaanthony/clir"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/pkg/clilogger"
@@ -72,6 +77,9 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
webview2 := "download"
command.StringFlag("webview2", "WebView2 installer strategy: download,embed,browser,error.", &webview2)
runDelve := false
command.BoolFlag("delve", "Runs the built binary in delve for debugging", &runDelve)
command.Action(func() error {
quiet := verbosity == 0
@@ -116,6 +124,12 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
compress = false
}
// Lookup compiler path
compilerPath, err := exec.LookPath(compilerCommand)
if err != nil {
return fmt.Errorf("unable to find compiler: %s", compilerCommand)
}
// Tags
userTags := []string{}
for _, tag := range strings.Split(tags, " ") {
@@ -144,6 +158,11 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
}
}
// If we want to use delve we need to compile in DEBUG mode
if runDelve {
mode = build.Debug
}
// Create BuildOptions
buildOptions := &build.Options{
Logger: logger,
@@ -160,16 +179,23 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
CompressFlags: compressFlags,
UserTags: userTags,
WebView2Strategy: wv2rtstrategy,
RunDelve: runDelve,
}
// Calculate platform and arch
platformSplit := strings.Split(platform, "/")
buildOptions.Platform = platformSplit[0]
buildOptions.Arch = runtime.GOARCH
if system.IsAppleSilicon() {
buildOptions.Arch = "arm64"
} else {
buildOptions.Arch = runtime.GOARCH
}
if len(platformSplit) == 2 {
buildOptions.Arch = platformSplit[1]
}
println("Build arch =", buildOptions.Arch)
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
@@ -184,7 +210,7 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
fmt.Fprintf(w, "App Type: \t%s\n", buildOptions.OutputType)
fmt.Fprintf(w, "Platform: \t%s\n", buildOptions.Platform)
fmt.Fprintf(w, "Arch: \t%s\n", buildOptions.Arch)
fmt.Fprintf(w, "Compiler: \t%s\n", buildOptions.Compiler)
fmt.Fprintf(w, "Compiler: \t%s\n", compilerPath)
fmt.Fprintf(w, "Compress: \t%t\n", buildOptions.Compress)
fmt.Fprintf(w, "Build Mode: \t%s\n", buildModeText)
fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack)
@@ -219,5 +245,40 @@ func doBuild(buildOptions *build.Options) error {
buildOptions.Logger.Println(fmt.Sprintf("Built '%s' in %s.", outputFilename, elapsed.Round(time.Millisecond).String()))
buildOptions.Logger.Println("")
if buildOptions.RunDelve {
// 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
buildOptions.Logger.Println("Launching Delve on port 2345...")
cmdArgs := slicer.String([]string{"--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", outputFilename})
if buildOptions.Verbosity == build.VERBOSE {
buildOptions.Logger.Println("\tRunning: dlv %s", cmdArgs.Join(" "))
}
stdout, stderr, err := shell.RunCommand(cwd, "dlv", cmdArgs.AsSlice()...)
if buildOptions.Verbosity == build.VERBOSE || err != nil {
trimstdout := strings.TrimSpace(stdout)
if trimstdout != "" {
buildOptions.Logger.Println(trimstdout)
}
trimstderr := strings.TrimSpace(stderr)
if trimstderr != "" {
buildOptions.Logger.Println(trimstderr)
}
}
if err != nil {
return err
}
}
return nil
}

View File

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

@@ -12,13 +12,14 @@ The next steps to complete the template are:
- `html` - path to your `index.html`
- `frontend:install` - The command to install your frontend dependencies
- `frontend:build` - The command to build your frontend
4. Delete this file.
4. Remove any `public` or `dist` directories.
5. Delete this file.
## Testing your template
You can test your template by running this command:
`wails init -name test -t /path/to/your/new/template`
`wails init -name test -t {{.TemplateDir}}`
### Checklist
Once generated, do the following tests:

View File

@@ -6,29 +6,29 @@ import (
"github.com/wailsapp/wails/v2"
)
// Basic application struct
type Basic struct {
// App struct
type App struct {
runtime *wails.Runtime
}
// NewBasic creates a new Basic application struct
func NewBasic() *Basic {
return &Basic{}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called at application startup
func (b *Basic) startup(runtime *wails.Runtime) {
func (b *App) startup(runtime *wails.Runtime) {
// Perform your setup here
b.runtime = runtime
runtime.Window.SetTitle("{{.ProjectName}}")
}
// shutdown is called at application termination
func (b *Basic) shutdown() {
func (b *App) shutdown() {
// Perform your teardown here
}
// Greet returns a greeting for the given name
func (b *Basic) Greet(name string) string {
func (b *App) Greet(name string) string {
return fmt.Sprintf("Hello %s!", name)
}

View File

@@ -15,7 +15,7 @@ import (
func main() {
// Create application with options
app := NewBasic()
app := NewApp()
err := wails.Run(&options.App{
Title: "{{.ProjectName}}",

View File

@@ -3,7 +3,7 @@
"outputfilename": "{{.BinaryName}}",
"html": "frontend/dist/index.html",
"frontend:build": "npm run build",
"frontend:install": "npm ci",
"frontend:install": "npm install",
"author": {
"name": "{{.AuthorName}}",
"email": "{{.AuthorEmail}}"

View File

@@ -12,6 +12,7 @@ import (
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/leaanthony/clir"
"github.com/tidwall/sjson"
)
//go:embed base
@@ -25,8 +26,8 @@ func AddSubCommand(app *clir.Cli, parent *clir.Command, w io.Writer) {
name := ""
command.StringFlag("name", "The name of the template", &name)
useLocalFilesAsFrontend := false
command.BoolFlag("frontend", "This indicates that the current directory is a frontend project and should be used by the template", &useLocalFilesAsFrontend)
migrate := false
command.BoolFlag("migrate", "This indicates that the current directory is a frontend project and should be used by the template", &migrate)
// Quiet Init
quiet := false
@@ -46,6 +47,7 @@ func AddSubCommand(app *clir.Cli, parent *clir.Command, w io.Writer) {
}
if !empty {
templateDir = filepath.Join(cwd, name)
println("Creating new template directory:", name)
err = fs.Mkdir(templateDir)
if err != nil {
return err
@@ -68,16 +70,21 @@ func AddSubCommand(app *clir.Cli, parent *clir.Command, w io.Writer) {
type templateData struct {
Name string
Description string
TemplateDir string
}
println("Extracting base template files...")
err = g.Extract(templateDir, &templateData{
Name: name,
Name: name,
TemplateDir: templateDir,
})
if err != nil {
return err
}
if useLocalFilesAsFrontend == false {
// If we aren't migrating the files, just exit
if migrate == false {
return nil
}
@@ -88,44 +95,94 @@ func AddSubCommand(app *clir.Cli, parent *clir.Command, w io.Writer) {
return err
}
err = fs.CopyDirExtended(cwd, frontendDir, []string{name})
// Move the files into a new frontend directory
println("Migrating files to frontend directory...")
err = fs.MoveDirExtended(cwd, frontendDir, []string{name})
if err != nil {
return err
}
//// Create logger
//logger := clilogger.New(w)
//logger.Mute(quiet)
//
//app.PrintBanner()
//
//logger.Print("Generating Javascript module for Go code...")
//
//// Start Time
//start := time.Now()
//
//p, err := parser.GenerateWailsFrontendPackage()
//if err != nil {
// return err
//}
//
//logger.Println("done.")
//logger.Println("")
//
//elapsed := time.Since(start)
//packages := p.Packages
//
//// Print report
//for _, pkg := range p.Packages {
// if pkg.ShouldGenerate() {
// generate.logPackage(pkg, logger)
// }
//
//}
//
//logger.Println("%d packages parsed in %s.", len(packages), elapsed)
// Process package.json
err = processPackageJSON(frontendDir)
if err != nil {
return err
}
// Process package-lock.json
err = processPackageLockJSON(frontendDir)
if err != nil {
return err
}
// Remove node_modules - ignore error, eg it doesn't exist
_ = os.RemoveAll(filepath.Join(frontendDir, "node_modules"))
return nil
})
}
func processPackageJSON(frontendDir string) error {
var err error
packageJSON := filepath.Join(frontendDir, "package.json")
if !fs.FileExists(packageJSON) {
println("No package.json found - cannot process.")
return nil
}
data, err := os.ReadFile(packageJSON)
if err != nil {
return err
}
json := string(data)
// We will ignore these errors - it's not critical
println("Updating package.json data...")
json, _ = sjson.Set(json, "name", "{{.ProjectName}}")
json, _ = sjson.Set(json, "author", "{{.AuthorName}}")
err = os.WriteFile(packageJSON, []byte(json), 0644)
if err != nil {
return err
}
baseDir := filepath.Dir(packageJSON)
println("Renaming package.json -> package.tmpl.json...")
err = os.Rename(packageJSON, filepath.Join(baseDir, "package.tmpl.json"))
if err != nil {
return err
}
return nil
}
func processPackageLockJSON(frontendDir string) error {
var err error
filename := filepath.Join(frontendDir, "package-lock.json")
if !fs.FileExists(filename) {
println("No package-lock.json found - cannot process.")
return nil
}
data, err := os.ReadFile(filename)
if err != nil {
return err
}
json := string(data)
// We will ignore these errors - it's not critical
println("Updating package-lock.json data...")
json, _ = sjson.Set(json, "name", "{{.ProjectName}}")
err = os.WriteFile(filename, []byte(json), 0644)
if err != nil {
return err
}
baseDir := filepath.Dir(filename)
println("Renaming package-lock.json -> package-lock.tmpl.json...")
err = os.Rename(filename, filepath.Join(baseDir, "package-lock.tmpl.json"))
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,24 @@
# README
## About
This is a basic Svelte template, using rollup to bundle the assets into a single JS file.
Rollup is configured to do the following:
- Convert imported images to base64 strings
- Convert `url()` in `@font-face` declarations to base64 strings
- Bundle all css into the JS bundle
- Copy `index.html` from `frontend/src/` to `frontend/dist/`
Clicking the button will call the backend
## Building
To build this project in debug mode, use `wails build`. For production, use `wails build -production`.
To generate a platform native package, add the `-package` flag.
## Live Development
To run in live development mode, run `wails dev` in the project directory. In another terminal, go into the `frontend`
directory and run `npm run dev`. The frontend dev server will run on http://localhost:5000. Connect to this
in your browser and connect to your application.

View File

@@ -6,29 +6,28 @@ import (
"github.com/wailsapp/wails/v2"
)
// Basic application struct
type Basic struct {
// App application struct
type App struct {
runtime *wails.Runtime
}
// NewBasic creates a new Basic application struct
func NewBasic() *Basic {
return &Basic{}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called at application startup
func (b *Basic) startup(runtime *wails.Runtime) {
func (b *App) startup(runtime *wails.Runtime) {
// Perform your setup here
b.runtime = runtime
runtime.Window.SetTitle("{{.ProjectName}}")
}
// shutdown is called at application termination
func (b *Basic) shutdown() {
func (b *App) shutdown() {
// Perform your teardown here
}
// Greet returns a greeting for the given name
func (b *Basic) Greet(name string) string {
func (b *App) Greet(name string) string {
return fmt.Sprintf("Hello %s!", name)
}

View File

@@ -0,0 +1,4 @@
/node_modules/
/dist/build/
.DS_Store

View File

@@ -0,0 +1,105 @@
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
---
# svelte app
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
```bash
npx degit sveltejs/template svelte-app
cd svelte-app
```
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
## Get started
Install the dependencies...
```bash
cd svelte-app
npm install
```
...then start [Rollup](https://rollupjs.org):
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
## Building and running in production mode
To create an optimised version of the app:
```bash
npm run build
```
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
## Single-page app mode
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
```js
"start": "sirv public --single"
```
## Using TypeScript
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
```bash
node scripts/setupTypeScript.js
```
Or remove the script via:
```bash
rm scripts/setupTypeScript.js
```
## Deploying to the web
### With [Vercel](https://vercel.com)
Install `vercel` if you haven't already:
```bash
npm install -g vercel
```
Then, from within your project folder:
```bash
cd public
vercel deploy --name my-project
```
### With [surge](https://surge.sh/)
Install `surge` if you haven't already:
```bash
npm install -g surge
```
Then, from within your project folder:
```bash
npm run build
surge public my-project.surge.sh
```

View File

@@ -0,0 +1,27 @@
{
"name": "{{.ProjectName}}",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv dist --no-clear"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-image": "^2.0.6",
"@rollup/plugin-node-resolve": "^11.0.0",
"postcss": "^8.3.5",
"postcss-url": "^10.1.3",
"rollup": "^2.3.4",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0"
},
"dependencies": {
"sirv-cli": "^1.0.0"
}
,"author":"{{.AuthorName}}"}

View File

@@ -0,0 +1,91 @@
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import copy from 'rollup-plugin-copy';
import image from '@rollup/plugin-image';
import postcss from 'rollup-plugin-postcss'
import url from 'postcss-url';
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'dist/bundle.js'
},
plugins: [
image({
include: './src/assets/images/**'
}),
svelte({
compilerOptions: {
// enable run-time checks when not in production
dev: !production
}
}),
postcss({
minimize: true,
plugins: [
url({
url: "inline"
})
]
}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
copy({
targets: [
{ src: 'src/index.html', dest: 'dist/' },
]
}),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `dist` directory and refresh the
// browser on changes when not in production
!production && livereload('dist'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
};

View File

@@ -0,0 +1,121 @@
// @ts-check
/** This script modifies the project to support TS code in .svelte files like:
<script lang="ts">
export let name: string;
</script>
As well as validating the code for CI.
*/
/** To work on this script:
rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
*/
const fs = require("fs")
const path = require("path")
const { argv } = require("process")
const projectRoot = argv[2] || path.join(__dirname, "..")
// Add deps to pkg.json
const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
"svelte-check": "^2.0.0",
"svelte-preprocess": "^4.0.0",
"@rollup/plugin-typescript": "^8.0.0",
"typescript": "^4.0.0",
"tslib": "^2.0.0",
"@tsconfig/svelte": "^2.0.0"
})
// Add script for checking
packageJSON.scripts = Object.assign(packageJSON.scripts, {
"check": "svelte-check --tsconfig ./tsconfig.json"
})
// Write the package JSON
fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
fs.renameSync(beforeMainJSPath, afterMainTSPath)
// Switch the app.svelte file to use TS
const appSveltePath = path.join(projectRoot, "src", "App.svelte")
let appFile = fs.readFileSync(appSveltePath, "utf8")
appFile = appFile.replace("<script>", '<script lang="ts">')
appFile = appFile.replace("export let name;", 'export let name: string;')
fs.writeFileSync(appSveltePath, appFile)
// Edit rollup config
const rollupConfigPath = path.join(projectRoot, "rollup.config.js")
let rollupConfig = fs.readFileSync(rollupConfigPath, "utf8")
// Edit imports
rollupConfig = rollupConfig.replace(`'rollup-plugin-terser';`, `'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import typescript from '@rollup/plugin-typescript';`)
// Replace name of entry point
rollupConfig = rollupConfig.replace(`'src/main.js'`, `'src/main.ts'`)
// Add preprocessor
rollupConfig = rollupConfig.replace(
'compilerOptions:',
'preprocess: sveltePreprocess({ sourceMap: !production }),\n\t\t\tcompilerOptions:'
);
// Add TypeScript
rollupConfig = rollupConfig.replace(
'commonjs(),',
'commonjs(),\n\t\ttypescript({\n\t\t\tsourceMap: !production,\n\t\t\tinlineSources: !production\n\t\t}),'
);
fs.writeFileSync(rollupConfigPath, rollupConfig)
// Add TSConfig
const tsconfig = `{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["src/**/*"],
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
}`
const tsconfigPath = path.join(projectRoot, "tsconfig.json")
fs.writeFileSync(tsconfigPath, tsconfig)
// Add global.d.ts
const dtsPath = path.join(projectRoot, "src", "global.d.ts")
fs.writeFileSync(dtsPath, `/// <reference types="svelte" />`)
// Delete this script, but not during testing
if (!argv[2]) {
// Remove the script
fs.unlinkSync(path.join(__filename))
// Check for Mac's DS_store file, and if it's the only one left remove it
const remainingFiles = fs.readdirSync(path.join(__dirname))
if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') {
fs.unlinkSync(path.join(__dirname, '.DS_store'))
}
// Check if the scripts folder is empty
if (fs.readdirSync(path.join(__dirname)).length === 0) {
// Remove the scripts folder
fs.rmdirSync(path.join(__dirname))
}
}
// Adds the extension recommendation
fs.mkdirSync(path.join(projectRoot, ".vscode"), { recursive: true })
fs.writeFileSync(path.join(projectRoot, ".vscode", "extensions.json"), `{
"recommendations": ["svelte.svelte-vscode"]
}
`)
console.log("Converted to TypeScript.")
if (fs.existsSync(path.join(projectRoot, "node_modules"))) {
console.log("\nYou will need to re-run your dependency manager to get started.")
}

View File

@@ -0,0 +1,57 @@
<script>
let name = "";
let greeting = "";
function greet() {
window.backend.main.App.Greet(name).then((result) => {
greeting = result;
});
}
</script>
<main>
<div id="logo"></div>
<div id="input" data-wails-no-drag>
<input id="name" type="text" bind:value={name}>
<button class="button" on:click={greet}>Greet</button>
</div>
{#if greeting}
<div id="result">{greeting}</div>
{/if}
</main>
<style>
main {
height: 100%;
width: 100%;
}
#result {
margin-top: 1rem;
font-size: 1.5rem;
}
button {
-webkit-appearance: default-button;
padding: 6px;
}
#name {
border-radius: 3px;
outline: none;
-webkit-font-smoothing: antialiased;
}
#logo {
width: 40%;
height: 40%;
padding-top: 20%;
margin: auto;
display: block;
background-position: center;
background-repeat: no-repeat;
background-image: url("assets/images/logo-dark.svg");
}
</style>

View File

@@ -0,0 +1,93 @@
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 551 436" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.27527,0,0,1.27527,104.01,410.563)">
<path d="M0,-51.891L14.429,-51.891L13.043,-21.183L22.568,-51.891L34.226,-51.891L34.084,-21.183L42.365,-51.891L56.794,-51.891L38.526,0L25.198,0L25.34,-32.45L15.211,0L1.919,0L0,-51.891Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(1.27527,0,0,1.27527,224.985,367.503)">
<path d="M0,15.639L5.793,15.639L5.971,-3.589L0,15.639ZM-20.187,33.765L-0.675,-18.126L16.42,-18.126L20.08,33.765L5.437,33.765L5.509,26.123L-3.057,26.123L-5.332,33.765L-20.187,33.765Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.19929,-1.2596,-1.2596,-0.19929,332.323,396.949)">
<path d="M-16.046,33.107L36.491,33.107L38.757,18.784L-13.785,18.82L-16.046,33.107Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(1.27527,0,0,1.27527,353.217,344.388)">
<path d="M0,51.891L8.246,0L22.781,0L16.597,39.024L27.224,39.024L25.199,51.891L0,51.891Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(1.27527,0,0,1.27527,427.939,364.922)">
<path d="M0,19.83C1.611,21.181 3.305,22.224 5.083,22.959C6.859,23.693 8.565,24.06 10.2,24.06C11.645,24.06 12.794,23.663 13.647,22.87C14.5,22.076 14.927,20.992 14.927,19.617C14.927,18.434 14.571,17.254 13.861,16.081C13.15,14.908 11.775,13.351 9.738,11.408C7.273,9.015 5.58,6.906 4.655,5.081C3.731,3.257 3.27,1.243 3.27,-0.96C3.27,-5.912 4.839,-9.846 7.979,-12.76C11.118,-15.674 15.377,-17.132 20.756,-17.132C22.936,-17.132 25.008,-16.889 26.975,-16.403C28.941,-15.917 30.943,-15.165 32.982,-14.146L30.92,-1.493C29.356,-2.583 27.834,-3.412 26.354,-3.981C24.872,-4.551 23.457,-4.835 22.106,-4.835C20.898,-4.835 19.943,-4.521 19.245,-3.894C18.546,-3.265 18.196,-2.406 18.196,-1.316C18.196,0.154 19.535,2.215 22.213,4.868C22.544,5.2 22.805,5.46 22.995,5.649C25.696,8.304 27.473,10.578 28.326,12.475C29.179,14.37 29.605,16.56 29.605,19.049C29.605,24.594 27.893,28.965 24.469,32.163C21.046,35.361 16.36,36.962 10.413,36.962C7.877,36.962 5.479,36.66 3.216,36.056C0.953,35.45 -0.948,34.615 -2.488,33.549L0,19.83Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(-166.599,4.57132,4.57132,166.599,147.403,167.648)">
<path d="M0.883,-0.081L0.121,0.081L0.256,-0.063L0.883,-0.081Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
<g transform="matrix(-106.443,-16.0669,-16.0669,106.443,428.19,188.033)">
<path d="M0.878,-0.285L-0.073,0.71L-1.186,0.542L0.015,0.207L-0.846,0.077L0.355,-0.258L-0.505,-0.388L0.649,-0.71L0.878,-0.285Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
</g>
<g transform="matrix(-114.484,-162.408,-162.408,114.484,333.291,285.804)">
<path d="M0.44,-0.04L0.44,-0.04L0.44,-0.04L0.265,-0.056L0.177,0.437L-0.311,-0.255L0.262,-0.437L0.568,-0.437L0.44,-0.04Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
</g>
<g transform="matrix(61.6919,58.8091,58.8091,-61.6919,258.631,180.413)">
<path d="M0.5,0L0.5,-0L0.5,0L0.5,0Z" style="fill:url(#_Linear4);fill-rule:nonzero;"/>
</g>
<g transform="matrix(238.126,298.893,298.893,-238.126,113.516,-150.536)">
<path d="M0.622,-0.115L0.761,-0.115L0.806,-0.013L0.826,0.182L0.622,-0.115Z" style="fill:url(#_Linear5);fill-rule:nonzero;"/>
</g>
<g transform="matrix(-369.529,-97.4118,-97.4118,369.529,582.38,94.027)">
<path d="M0.467,0.005L0.49,0.062L0.271,-0.062L0.467,0.005Z" style="fill:url(#_Linear6);fill-rule:nonzero;"/>
</g>
<g transform="matrix(-496.156,-53.9751,-53.9751,496.156,367.888,125.085)">
<path d="M0.2,0.001L0.219,-0.018L0.614,0.012L0.519,0.089L0.282,0.068L0.2,0.135L0.463,0.194L0.374,0.266L0.138,0.186L0.138,0.186L0.138,0.186L0.047,0.033L-0.131,-0.266L0.2,0.001Z" style="fill:url(#_Linear7);fill-rule:nonzero;"/>
</g>
<g transform="matrix(185.076,176.427,176.427,-185.076,153.446,80.1488)">
<path d="M0.735,-0L0.735,-0L0.735,0L0.735,-0Z" style="fill:url(#_Linear8);fill-rule:nonzero;"/>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,-3.46945e-18,-3.46945e-18,-1,0,-3.05761e-06)"><stop offset="0" style="stop-color:rgb(227,50,50);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(107,0,13);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,0,0,-1,0,-2.75467e-06)"><stop offset="0" style="stop-color:rgb(227,50,50);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(107,0,13);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,-1.11022e-16,-1.11022e-16,-1,0,-2.61861e-06)"><stop offset="0" style="stop-color:rgb(227,50,50);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(107,0,13);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,-5.55112e-17,-5.55112e-17,-1,0,-1.57562e-06)"><stop offset="0" style="stop-color:rgb(227,50,50);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(107,0,13);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.801899,-0.59746,-0.59746,0.801899,1.3495,0.447457)"><stop offset="0" style="stop-color:rgb(227,50,50);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(107,0,13);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,-2.77556e-17,-2.77556e-17,-1,0,-1.92826e-06)"><stop offset="0" style="stop-color:rgb(227,50,50);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(107,0,13);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear7" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,0,0,-1,0,9.68429e-07)"><stop offset="0" style="stop-color:rgb(227,50,50);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(107,0,13);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear8" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,0,0,-1,0,1.43665e-07)"><stop offset="0" style="stop-color:rgb(227,50,50);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(107,0,13);stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,24 @@
html {
text-align: center;
color: white;
background-color: rgba(1,1,1,0.1);
width: 100%;
height: 100%;
}
body {
color: white;
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
margin: 0;
width: 100%;
height: 100%;
}
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 400;
src: local(''),
url('./assets/fonts/nunito-v16-latin-regular.woff2') format('woff2')
}

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte app</title>
<script defer src='bundle.js'></script>
</head>
<body data-wails-drag>
</body>
</html>

View File

@@ -0,0 +1,9 @@
import App from './App.svelte';
import './global.css';
const app = new App({
target: document.body,
});
export default app;

View File

@@ -0,0 +1,9 @@
module test
go 1.16
require (
github.com/wailsapp/wails/v2 v2.0.0-alpha
)
replace github.com/wailsapp/wails/v2 v2.0.0-alpha => {{.WailsDirectory}}

View File

@@ -0,0 +1,56 @@
package main
import (
"log"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
)
func main() {
// Create application with options
app := NewApp()
err := wails.Run(&options.App{
Title: "{{.ProjectName}}",
Width: 800,
Height: 600,
MinWidth: 400,
MinHeight: 400,
MaxWidth: 1280,
MaxHeight: 1024,
DisableResize: false,
Fullscreen: false,
Frameless: false,
StartHidden: false,
HideWindowOnClose: false,
DevTools: false,
RGBA: 0x000000FF,
Windows: &windows.Options{
WebviewIsTransparent: true,
WindowBackgroundIsTranslucent: true,
DisableWindowIcon: true,
},
Mac: &mac.Options{
WebviewIsTransparent: true,
WindowBackgroundIsTranslucent: true,
TitleBar: mac.TitleBarHiddenInset(),
Menu: menu.DefaultMacMenu(),
},
LogLevel: logger.DEBUG,
Startup: app.startup,
Shutdown: app.shutdown,
Bind: []interface{}{
app,
},
})
if err != nil {
log.Fatal(err)
}
}

View File

@@ -0,0 +1,7 @@
{
"name": "Basic Svelte + Rollup",
"shortname": "svelte",
"author": "Lea Anthony",
"description": "Svelte template using rollup to bundle css, images and fonts",
"helpurl": "https://github.com/wailsapp/wails"
}

View File

@@ -0,0 +1,11 @@
{
"name": "{{.ProjectName}}",
"outputfilename": "{{.BinaryName}}",
"html": "frontend/dist/index.html",
"frontend:build": "npm run build",
"frontend:install": "npm install",
"author": {
"name": "{{.AuthorName}}",
"email": "{{.AuthorEmail}}"
}
}

View File

@@ -0,0 +1,33 @@
package main
import (
"fmt"
"github.com/wailsapp/wails/v2"
)
// App struct
type App struct {
runtime *wails.Runtime
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called at application startup
func (b *App) startup(runtime *wails.Runtime) {
// Perform your setup here
b.runtime = runtime
}
// shutdown is called at application termination
func (b *App) shutdown() {
// Perform your teardown here
}
// Greet returns a greeting for the given name
func (b *App) Greet(name string) string {
return fmt.Sprintf("Hello %s!", name)
}

View File

@@ -12,9 +12,9 @@ ready( () => {
// Get name
let name = nameElement.value;
// Call Basic.Greet(name)
window.backend.main.Basic.Greet(name).then((result) => {
// Update result with data back from Basic.Greet()
// Call App.Greet(name)
window.backend.main.App.Greet(name).then((result) => {
// Update result with data back from App.Greet()
document.getElementById("result").innerText = result;
});
};

View File

@@ -1,9 +1,10 @@
package main
import (
"github.com/wailsapp/wails/v2/pkg/options/windows"
"log"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
@@ -14,7 +15,7 @@ import (
func main() {
// Create application with options
app := NewBasic()
app := NewApp()
err := wails.Run(&options.App{
Title: "{{.ProjectName}}",

View File

@@ -3,7 +3,7 @@
"outputfilename": "{{.BinaryName}}",
"html": "frontend/dist/index.html",
"frontend:build": "npm run build",
"frontend:install": "npm ci",
"frontend:install": "npm install",
"author": {
"name": "{{.AuthorName}}",
"email": "{{.AuthorEmail}}"

View File

@@ -10,7 +10,6 @@ import (
"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"
@@ -40,10 +39,6 @@ func main() {
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())

View File

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

View File

@@ -19,6 +19,7 @@ require (
github.com/leaanthony/go-ansi-parser v1.0.1
github.com/leaanthony/go-common-file-dialog v1.0.3
github.com/leaanthony/gosod v1.0.1
github.com/leaanthony/idgen v1.0.0
github.com/leaanthony/slicer v1.5.0
github.com/leaanthony/webview2runtime v1.1.0
github.com/leaanthony/winicon v0.0.0-20200606125418-4419cea822a0
@@ -30,8 +31,10 @@ require (
github.com/tdewolff/minify v2.3.6+incompatible
github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/test v1.0.6 // indirect
github.com/tidwall/sjson v1.1.7
github.com/wzshiming/ctc v1.2.3
github.com/xyproto/xpm v1.2.1
github.com/ztrue/tracerr v0.3.0
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
golang.org/x/mod v0.4.1 // indirect
golang.org/x/net v0.0.0-20210326060303-6b1517762897

View File

@@ -99,6 +99,8 @@ github.com/leaanthony/go-common-file-dialog v1.0.3 h1:O0uGjKnWtdEADGrkg+TyAAbZyl
github.com/leaanthony/go-common-file-dialog v1.0.3/go.mod h1:TGhEc9eSJgRsupZ+iH1ZgAOnEo9zp05cRH2j08RPrF0=
github.com/leaanthony/gosod v1.0.1 h1:F+4c3DmEBfigi7oAswCV2RpQ+k4DcNbhuCZUGdBHacQ=
github.com/leaanthony/gosod v1.0.1/go.mod h1:W8RyeSFBXu7RpIxPGEJfW4moSyGGEjlJMLV25wEbAdU=
github.com/leaanthony/idgen v1.0.0 h1:IZreR+JGEzFV4yeVuBZA25gM0keUoFy+RDUldncQ+Jw=
github.com/leaanthony/idgen v1.0.0/go.mod h1:4nBZnt8ml/f/ic/EVQuLxuj817RccT2fyrUaZFxrcVA=
github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/leaanthony/webview2runtime v1.1.0 h1:N0pv55ift8XtqozIp4PNOtRCJ/Qdd/qzx80lUpalS4c=
@@ -107,6 +109,8 @@ github.com/leaanthony/winicon v0.0.0-20200606125418-4419cea822a0 h1:FPGYnfxuuxqC
github.com/leaanthony/winicon v0.0.0-20200606125418-4419cea822a0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
@@ -145,6 +149,14 @@ github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxp
github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ=
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tidwall/gjson v1.8.0 h1:Qt+orfosKn0rbNTZqHYDqBrmm3UDA4KRkv70fDzG+PQ=
github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/sjson v1.1.7 h1:sgVPwu/yygHJ2m1pJDLgGM/h+1F5odx5Q9ljG3imRm8=
github.com/tidwall/sjson v1.1.7/go.mod h1:w/yG+ezBeTdUxiKs5NcPicO9diP38nk96QBAbIIGeFs=
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=
@@ -158,6 +170,8 @@ github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6e
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=
github.com/ztrue/tracerr v0.3.0 h1:lDi6EgEYhPYPnKcjsYzmWw4EkFEoA/gfe+I9Y5f+h6Y=
github.com/ztrue/tracerr v0.3.0/go.mod h1:qEalzze4VN9O8tnhBXScfCrmoJo10o8TN5ciKjm6Mww=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@@ -69,7 +69,8 @@ func CreateApp(appoptions *options.App) (*App, error) {
menuManager := menumanager.NewManager()
// Process the application menu
menuManager.SetApplicationMenu(options.GetApplicationMenu(appoptions))
appMenu := options.GetApplicationMenu(appoptions)
menuManager.SetApplicationMenu(appMenu)
// Process context menus
contextMenus := options.GetContextMenus(appoptions)

View File

@@ -117,6 +117,7 @@ func (a *App) Run() error {
var subsystemWaitGroup sync.WaitGroup
parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup)
ctx, cancel := context.WithCancel(parentContext)
defer cancel()
// Start the service bus
a.servicebus.Debug()
@@ -125,7 +126,7 @@ func (a *App) Run() error {
return err
}
runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback, nil)
runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback)
if err != nil {
return err
}

View File

@@ -9,11 +9,18 @@ import (
func (a *App) PreflightChecks(options *options.App) error {
_ = options
// Process the webview2 runtime situation. We can pass a strategy in via the `webview2` flag for `wails build`.
// This will determine how wv2runtime.Process will handle a lack of valid runtime.
err := wv2runtime.Process()
installedVersion, err := wv2runtime.Process()
if installedVersion != nil {
a.logger.Debug("WebView2 Runtime installed: Name: '%s' Version:'%s' Location:'%s'. Minimum version required: %s.",
installedVersion.Name, installedVersion.Version, installedVersion.Location, wv2runtime.MinimumRuntimeVersion)
}
if err != nil {
return err
}
return nil
}

View File

@@ -34,7 +34,15 @@ func (b BridgeClient) CallResult(message string) {
b.session.sendMessage("c" + message)
}
func (b BridgeClient) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
func (b BridgeClient) OpenFileDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
// Handled by dialog_client
}
func (b BridgeClient) OpenMultipleFilesDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
// Handled by dialog_client
}
func (b BridgeClient) OpenDirectoryDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
// Handled by dialog_client
}

View File

@@ -37,7 +37,11 @@ func (d *DialogClient) NotifyEvent(message string) {
func (d *DialogClient) CallResult(message string) {
}
func (d *DialogClient) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
func (d *DialogClient) OpenDirectoryDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
}
func (d *DialogClient) OpenFileDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
}
func (d *DialogClient) OpenMultipleFilesDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
}
func (d *DialogClient) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string) {

View File

@@ -284,8 +284,8 @@ func (c *Client) DarkModeEnabled(callbackID string) {
}
// SetApplicationMenu sets the application menu
func (c *Client) SetApplicationMenu(applicationMenuJSON string) {
C.SetApplicationMenu(c.app.app, c.app.string2CString(applicationMenuJSON))
func (c *Client) SetApplicationMenu(_ string) {
c.updateApplicationMenu()
}
// SetTrayMenu sets the tray menu

View File

@@ -2,6 +2,7 @@
// License included in README.md
#include "ffenestri_windows.h"
#include "shellscalingapi.h"
#include "wv2ComHandler_windows.h"
#include <functional>
#include <atomic>
@@ -42,6 +43,30 @@ char* LPWSTRToCstr(LPWSTR input) {
return output;
}
// Credit: https://building.enlyze.com/posts/writing-win32-apps-like-its-2020-part-3/
typedef int (__cdecl *PGetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE,UINT*,UINT*);
void getDPIForWindow(struct Application *app)
{
HMODULE hShcore = LoadLibraryW(L"shcore");
if (hShcore)
{
PGetDpiForMonitor pGetDpiForMonitor = reinterpret_cast<PGetDpiForMonitor>(GetProcAddress(hShcore, "GetDpiForMonitor"));
if (pGetDpiForMonitor)
{
HMONITOR hMonitor = MonitorFromWindow(app->window, MONITOR_DEFAULTTOPRIMARY);
pGetDpiForMonitor(hMonitor, (MONITOR_DPI_TYPE)0, &app->dpix, &app->dpiy);
}
} else {
// We couldn't get the window's DPI above, so get the DPI of the primary monitor
// using an API that is available in all Windows versions.
HDC hScreenDC = GetDC(0);
app->dpix = GetDeviceCaps(hScreenDC, LOGPIXELSX);
app->dpiy = GetDeviceCaps(hScreenDC, LOGPIXELSY);
ReleaseDC(0, hScreenDC);
}
}
struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose) {
// Create application
@@ -88,6 +113,9 @@ struct Application *NewApplication(const char *title, int width, int height, int
// Used to remember the window location when going fullscreen
result->previousPlacement = { sizeof(result->previousPlacement) };
// DPI
result->dpix = result->dpiy = 0;
return result;
}
@@ -142,6 +170,14 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch(msg) {
case WM_CREATE: {
createApplicationMenu(hwnd);
break;
}
case WM_COMMAND:
menuClicked(LOWORD(wParam));
break;
case WM_CLOSE: {
DestroyWindow( app->window );
break;
@@ -171,30 +207,28 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
return 0;
}
// get pixel density
HDC hDC = GetDC(NULL);
double DPIScaleX = GetDeviceCaps(hDC, 88)/96.0;
double DPIScaleY = GetDeviceCaps(hDC, 90)/96.0;
ReleaseDC(NULL, hDC);
// update DPI
getDPIForWindow(app);
double DPIScaleX = app->dpix/96.0;
double DPIScaleY = app->dpiy/96.0;
RECT rcClient, rcWind;
RECT rcWind;
POINT ptDiff;
GetClientRect(hwnd, &rcClient);
GetWindowRect(hwnd, &rcWind);
int widthExtra = (rcWind.right - rcWind.left) - rcClient.right;
int heightExtra = (rcWind.bottom - rcWind.top) - rcClient.bottom;
int widthExtra = (rcWind.right - rcWind.left);
int heightExtra = (rcWind.bottom - rcWind.top);
LPMINMAXINFO mmi = (LPMINMAXINFO) lParam;
if (app->minWidth > 0 && app->minHeight > 0) {
mmi->ptMinTrackSize.x = app->minWidth * DPIScaleX + widthExtra;
mmi->ptMinTrackSize.y = app->minHeight * DPIScaleY + heightExtra;
mmi->ptMinTrackSize.x = app->minWidth * DPIScaleX;
mmi->ptMinTrackSize.y = app->minHeight * DPIScaleY;
}
if (app->maxWidth > 0 && app->maxHeight > 0) {
mmi->ptMaxSize.x = app->maxWidth * DPIScaleX + widthExtra;
mmi->ptMaxSize.y = app->maxHeight * DPIScaleY + heightExtra;
mmi->ptMaxTrackSize.x = app->maxWidth * DPIScaleX + widthExtra;
mmi->ptMaxTrackSize.y = app->maxHeight * DPIScaleY + heightExtra;
mmi->ptMaxSize.x = app->maxWidth * DPIScaleX;
mmi->ptMaxSize.y = app->maxHeight * DPIScaleY;
mmi->ptMaxTrackSize.x = app->maxWidth * DPIScaleX;
mmi->ptMaxTrackSize.y = app->maxHeight * DPIScaleY;
}
return 0;
}
@@ -294,7 +328,6 @@ void completed(struct Application* app) {
messageFromWindowCallback(readyMessage.c_str());
}
//
bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb) {
@@ -384,6 +417,7 @@ bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb)
}
app->webviewController = controller;
app->webview = webview;
// Resize WebView to fit the bounds of the parent window
RECT bounds;
GetClientRect(app->window, &bounds);
@@ -395,7 +429,64 @@ bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb)
LPCWSTR html = (LPCWSTR) cstrToLPWSTR((char*)assets[0]);
app->webview->Navigate(html);
if( app->webviewIsTranparent ) {
wchar_t szBuff[64];
ICoreWebView2Controller2 *wc2;
wc2 = nullptr;
app->webviewController->QueryInterface(IID_ICoreWebView2Controller2, (void**)&wc2);
COREWEBVIEW2_COLOR wvColor;
wvColor.R = app->backgroundColour.R;
wvColor.G = app->backgroundColour.G;
wvColor.B = app->backgroundColour.B;
wvColor.A = app->backgroundColour.A == 0 ? 0 : 255;
if( app->windowBackgroundIsTranslucent ) {
wvColor.A = 0;
}
HRESULT result = wc2->put_DefaultBackgroundColor(wvColor);
if (!SUCCEEDED(result))
{
switch (result)
{
case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
{
MessageBox(
app->window,
L"Couldn't find Edge installation. "
"Do you have a version installed that's compatible with this "
"WebView2 SDK version?",
nullptr, MB_OK);
}
break;
case HRESULT_FROM_WIN32(ERROR_FILE_EXISTS):
{
MessageBox(
app->window, L"User data folder cannot be created because a file with the same name already exists.", nullptr, MB_OK);
}
break;
case E_ACCESSDENIED:
{
MessageBox(
app->window, L"Unable to create user data folder, Access Denied.", nullptr, MB_OK);
}
break;
case E_FAIL:
{
MessageBox(
app->window, L"Edge runtime unable to start", nullptr, MB_OK);
}
break;
default:
{
MessageBox(app->window, L"Failed to create WebView2 environment", nullptr, MB_OK);
}
}
}
}
messageFromWindowCallback("Ej{\"name\":\"wails:launched\",\"data\":[]}");
return true;
}
@@ -492,61 +583,6 @@ void Run(struct Application* app, int argc, char **argv) {
// Add webview2
initWebView2(app, debug, initialCallback);
if( app->webviewIsTranparent ) {
wchar_t szBuff[64];
ICoreWebView2Controller2 *wc2;
wc2 = nullptr;
app->webviewController->QueryInterface(IID_ICoreWebView2Controller2, (void**)&wc2);
COREWEBVIEW2_COLOR wvColor;
wvColor.R = app->backgroundColour.R;
wvColor.G = app->backgroundColour.G;
wvColor.B = app->backgroundColour.B;
wvColor.A = app->backgroundColour.A == 0 ? 0 : 255;
if( app->windowBackgroundIsTranslucent ) {
wvColor.A = 0;
}
HRESULT result = wc2->put_DefaultBackgroundColor(wvColor);
if (!SUCCEEDED(result))
{
switch (result)
{
case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
{
MessageBox(
app->window,
L"Couldn't find Edge installation. "
"Do you have a version installed that's compatible with this "
"WebView2 SDK version?",
nullptr, MB_OK);
}
break;
case HRESULT_FROM_WIN32(ERROR_FILE_EXISTS):
{
MessageBox(
app->window, L"User data folder cannot be created because a file with the same name already exists.", nullptr, MB_OK);
}
break;
case E_ACCESSDENIED:
{
MessageBox(
app->window, L"Unable to create user data folder, Access Denied.", nullptr, MB_OK);
}
break;
case E_FAIL:
{
MessageBox(
app->window, L"Edge runtime unable to start", nullptr, MB_OK);
}
break;
default:
{
MessageBox(app->window, L"Failed to create WebView2 environment", nullptr, MB_OK);
}
}
}
}
// Main event loop
MSG msg;

View File

@@ -13,9 +13,30 @@ extern void DisableWindowIcon(struct Application* app);
*/
import "C"
import (
"github.com/ztrue/tracerr"
"os"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// Setup the global caches
var globalCheckboxCache = NewCheckboxCache()
var globalRadioGroupCache = NewRadioGroupCache()
var globalRadioGroupMap = NewRadioGroupMap()
var globalApplicationMenu *Menu
type menuType string
const (
appMenuType menuType = "ApplicationMenu"
contextMenuType
trayMenuType
)
func (a *Application) processPlatformSettings() error {
menuManager = a.menuManager
config := a.config.Windows
if config == nil {
return nil
@@ -34,12 +55,9 @@ func (a *Application) processPlatformSettings() error {
C.DisableWindowIcon(a.app)
}
//// Process menu
////applicationMenu := options.GetApplicationMenu(a.config)
//applicationMenu := a.menuManager.GetApplicationMenuJSON()
//if applicationMenu != "" {
// C.SetApplicationMenu(a.app, a.string2CString(applicationMenu))
//}
// Unfortunately, we need to store this in the package variable so the C callback can see it
applicationMenu = a.menuManager.GetProcessedApplicationMenu()
//
//// Process tray
//trays, err := a.menuManager.GetTrayMenus()
@@ -70,3 +88,98 @@ func (a *Application) processPlatformSettings() error {
return nil
}
func (c *Client) updateApplicationMenu() {
applicationMenu = c.app.menuManager.GetProcessedApplicationMenu()
createApplicationMenu(uintptr(C.GetWindowHandle(c.app.app)))
}
/* ---------------------------------------------------------------------------------
Application Menu
----------------
There's only 1 application menu and this is where we create it. This method
is called from C after the window is created and the WM_CREATE message has
been sent.
*/
func checkFatal(err error) {
if err != nil {
tracerr.PrintSourceColor(err)
globalRadioGroupCache.Dump()
globalRadioGroupMap.Dump()
os.Exit(1)
}
}
//export createApplicationMenu
func createApplicationMenu(hwnd uintptr) {
if applicationMenu == nil {
return
}
var err error
window := win32Window(hwnd)
if globalApplicationMenu != nil {
checkFatal(globalApplicationMenu.Destroy())
}
globalApplicationMenu, err = createMenu(applicationMenu, appMenuType)
checkFatal(err)
err = setWindowMenu(window, globalApplicationMenu.menu)
checkFatal(err)
}
/*
This method is called by C when a menu item is pressed
*/
//export menuClicked
func menuClicked(id uint32) {
win32MenuID := win32MenuItemID(id)
//println("Got click from menu id", win32MenuID)
// Get the menu from the cache
menuItemDetails := getMenuCacheEntry(win32MenuID)
wailsMenuID := wailsMenuItemID(menuItemDetails.item.ID)
//println("Got click from menu id", win32MenuID, "- wails menu ID", wailsMenuID)
//spew.Dump(menuItemDetails)
switch menuItemDetails.item.Type {
case menu.CheckboxType:
// Determine if the menu is set or not
res, _, err := win32GetMenuState.Call(uintptr(menuItemDetails.parent), uintptr(id), uintptr(MF_BYCOMMAND))
if int(res) == -1 {
checkFatal(err)
}
flag := MF_CHECKED
if uint32(res) == MF_CHECKED {
flag = MF_UNCHECKED
}
for _, menuid := range globalCheckboxCache.win32MenuIDsForWailsMenuID(wailsMenuID) {
//println("setting menuid", menuid, "with flag", flag)
menuItemDetails := getMenuCacheEntry(menuid)
res, _, err = win32CheckMenuItem.Call(uintptr(menuItemDetails.parent), uintptr(menuid), uintptr(flag))
if int(res) == -1 {
checkFatal(err)
}
}
case menu.RadioType:
err := selectRadioItemFromWailsMenuID(wailsMenuID, win32MenuID)
checkFatal(err)
}
// Print the click error - it's not fatal
err := menuManager.ProcessClick(menuItemDetails.item.ID, "", string(menuItemDetails.menuType), "")
if err != nil {
println(err.Error())
}
}

View File

@@ -55,6 +55,10 @@ struct Application{
// placeholders
char* bindings;
char* initialCode;
// DPI
UINT dpix;
UINT dpiy;
};
#define ON_MAIN_THREAD(code) dispatch( [=]{ code; } )
@@ -80,6 +84,8 @@ extern "C" {
void DisableWindowIcon(struct Application* app);
void messageFromWindowCallback(const char *);
void* GetWindowHandle(struct Application*);
void createApplicationMenu(HWND hwnd);
void menuClicked(UINT id);
}
#endif

View File

@@ -140,7 +140,6 @@ void ShowTrayMenu(TrayMenu* trayMenu) {
// Create delegate
id trayMenuDelegate = msg_reg((id)trayMenuDelegateClass, s("new"));
msg_reg(trayMenuDelegate, s("autorelease"));
msg_id(menu, s("setDelegate:"), trayMenuDelegate);
objc_setAssociatedObject(trayMenuDelegate, "menu", menu, OBJC_ASSOCIATION_ASSIGN);
@@ -156,6 +155,10 @@ void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) {
// Delete the old menu
DeleteMenu(currentMenu->menu);
if( currentMenu->delegate != NULL ) {
msg_reg(currentMenu->delegate, s("release"));
currentMenu->delegate = NULL;
}
// Set the new one
currentMenu->menu = newMenu->menu;
@@ -179,6 +182,10 @@ void DeleteTrayMenu(TrayMenu* trayMenu) {
// Delete the menu
DeleteMenu(trayMenu->menu);
if( trayMenu->delegate != NULL ) {
msg_reg(trayMenu->delegate, s("release"));
trayMenu->delegate = NULL;
}
// Free JSON
if (trayMenu->processedJSON != NULL ) {
@@ -200,6 +207,10 @@ void DeleteTrayMenuKeepStatusBarItem(TrayMenu* trayMenu) {
// Delete the menu
DeleteMenu(trayMenu->menu);
if( trayMenu->delegate != NULL ) {
msg_reg(trayMenu->delegate, s("release"));
trayMenu->delegate = NULL;
}
// Free JSON
if (trayMenu->processedJSON != NULL ) {

View File

@@ -4,7 +4,7 @@ import (
"github.com/leaanthony/webview2runtime"
)
const minimumRuntimeVersion string = "91.0.864.48"
const MinimumRuntimeVersion string = "91.0.864.48"
type installationStatus int
@@ -14,21 +14,21 @@ const (
installed
)
func Process() error {
func Process() (*webview2runtime.Info, error) {
installStatus := needsInstalling
installedVersion := webview2runtime.GetInstalledVersion()
if installedVersion != nil {
installStatus = installed
updateRequired, err := installedVersion.IsOlderThan(minimumRuntimeVersion)
updateRequired, err := installedVersion.IsOlderThan(MinimumRuntimeVersion)
if err != nil {
_ = webview2runtime.Error(err.Error(), "Error")
return err
return installedVersion, err
}
// Installed and does not require updating
if !updateRequired {
return nil
return installedVersion, nil
}
installStatus = needsUpdating
}
return doInstallationStrategy(installStatus)
return installedVersion, doInstallationStrategy(installStatus)
}

View File

@@ -0,0 +1,93 @@
//+build windows
package ffenestri
import (
"fmt"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/menumanager"
"os"
"sync"
"text/tabwriter"
)
/* ---------------------------------------------------------------------------------
Checkbox Cache
--------------
The checkbox cache keeps a list of IDs that are associated with the same checkbox menu item.
This can happen when a checkbox is used in an application menu and a tray menu, eg "start at login".
The cache is used to bulk toggle the menu items when one is clicked.
*/
type CheckboxCache struct {
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID
mutex sync.RWMutex
}
func NewCheckboxCache() *CheckboxCache {
return &CheckboxCache{
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID),
}
}
func (c *CheckboxCache) Dump() {
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
println("---------------- Checkbox", c, "Dump ----------------")
for _, processedMenu := range c.cache {
println("Menu", processedMenu)
for wailsMenuItemID, win32menus := range processedMenu {
println(" WailsMenu: ", wailsMenuItemID)
menus := slicer.String()
for _, win32menu := range win32menus {
menus.Add(fmt.Sprintf("%v", win32menu))
}
_, _ = fmt.Fprintf(w, "%s\t%s\n", wailsMenuItemID, menus.Join(", "))
_ = w.Flush()
}
}
}
func (c *CheckboxCache) addToCheckboxCache(menu *menumanager.ProcessedMenu, item wailsMenuItemID, menuID win32MenuItemID) {
// Get map for menu
if c.cache[menu] == nil {
c.cache[menu] = make(map[wailsMenuItemID][]win32MenuItemID)
}
menuMap := c.cache[menu]
// Ensure we have a slice
if menuMap[item] == nil {
menuMap[item] = []win32MenuItemID{}
}
c.mutex.Lock()
menuMap[item] = append(menuMap[item], menuID)
c.mutex.Unlock()
}
func (c *CheckboxCache) removeMenuFromCheckboxCache(menu *menumanager.ProcessedMenu) {
c.mutex.Lock()
delete(c.cache, menu)
c.mutex.Unlock()
}
// win32MenuIDsForWailsMenuID returns all win32menuids that are used for a wails menu item id across
// all menus
func (c *CheckboxCache) win32MenuIDsForWailsMenuID(item wailsMenuItemID) []win32MenuItemID {
c.mutex.Lock()
result := []win32MenuItemID{}
for _, menu := range c.cache {
ids := menu[item]
if ids != nil {
result = append(result, ids...)
}
}
c.mutex.Unlock()
return result
}

View File

@@ -0,0 +1,28 @@
//+build windows,debug
package ffenestri
import (
"fmt"
"github.com/ztrue/tracerr"
"runtime"
"strings"
)
func wall(err error, inputs ...interface{}) error {
if err == nil {
return nil
}
pc, _, _, _ := runtime.Caller(1)
funcName := runtime.FuncForPC(pc).Name()
splitName := strings.Split(funcName, ".")
message := "[" + splitName[len(splitName)-1] + "]"
if len(inputs) > 0 {
params := []string{}
for _, param := range inputs {
params = append(params, fmt.Sprintf("%v", param))
}
message += "(" + strings.Join(params, " ") + ")"
}
return tracerr.Errorf(message)
}

View File

@@ -0,0 +1,47 @@
// +build windows,!debug
package ffenestri
import "C"
import (
"fmt"
"golang.org/x/sys/windows"
"log"
"os"
"runtime"
"strings"
"syscall"
)
func wall(err error, inputs ...interface{}) error {
if err == nil {
return nil
}
pc, _, _, _ := runtime.Caller(1)
funcName := runtime.FuncForPC(pc).Name()
splitName := strings.Split(funcName, ".")
message := "[" + splitName[len(splitName)-1] + "]"
if len(inputs) > 0 {
params := []string{}
for _, param := range inputs {
params = append(params, fmt.Sprintf("%v", param))
}
message += "(" + strings.Join(params, " ") + ")"
}
title, err := syscall.UTF16PtrFromString("Fatal Error")
if err != nil {
log.Fatal(err)
}
text, err := syscall.UTF16PtrFromString("There has been a fatal error. Details:\n" + message)
if err != nil {
log.Fatal(err)
}
var flags uint32 = windows.MB_ICONERROR | windows.MB_OK
_, err = windows.MessageBox(0, text, title, flags|windows.MB_SYSTEMMODAL)
os.Exit(1)
return err
}

View File

@@ -0,0 +1,191 @@
//+build windows
package ffenestri
import (
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/pkg/menu"
)
//-------------------- Types ------------------------
type win32MenuItemID uint32
type win32Menu uintptr
type win32Window uintptr
type wailsMenuItemID string // The internal menu ID
type Menu struct {
wailsMenu *menumanager.WailsMenu
menu win32Menu
menuType menuType
// A list of all checkbox and radio menuitems we
// create for this menu
checkboxes []win32MenuItemID
radioboxes []win32MenuItemID
initiallySelectedRadioItems []win32MenuItemID
}
func createMenu(wailsMenu *menumanager.WailsMenu, menuType menuType) (*Menu, error) {
mainMenu, err := createWin32Menu()
if err != nil {
return nil, err
}
result := &Menu{
wailsMenu: wailsMenu,
menu: mainMenu,
menuType: menuType,
}
// Process top level menus
for _, toplevelmenu := range applicationMenu.Menu.Items {
err := result.processMenuItem(result.menu, toplevelmenu)
if err != nil {
return nil, err
}
}
err = result.processRadioGroups()
if err != nil {
return nil, err
}
return result, nil
}
func (m *Menu) processMenuItem(parent win32Menu, menuItem *menumanager.ProcessedMenuItem) error {
// Ignore hidden items
if menuItem.Hidden {
return nil
}
// Calculate the flags for this menu item
flags := uintptr(calculateFlags(menuItem))
switch menuItem.Type {
case menu.SubmenuType:
submenu, err := createWin32PopupMenu()
if err != nil {
return err
}
for _, submenuItem := range menuItem.SubMenu.Items {
err = m.processMenuItem(submenu, submenuItem)
if err != nil {
return err
}
}
err = appendWin32MenuItem(parent, flags, uintptr(submenu), menuItem.Label)
if err != nil {
return err
}
case menu.TextType, menu.CheckboxType, menu.RadioType:
win32ID := addMenuCacheEntry(parent, m.menuType, menuItem, m.wailsMenu.Menu)
//label := fmt.Sprintf("%s (%d)", menuItem.Label, win32ID)
label := menuItem.Label
err := appendWin32MenuItem(parent, flags, uintptr(win32ID), label)
if err != nil {
return err
}
if menuItem.Type == menu.CheckboxType {
// We need to maintain a list of this menu's checkboxes
m.checkboxes = append(m.checkboxes, win32ID)
globalCheckboxCache.addToCheckboxCache(m.wailsMenu.Menu, wailsMenuItemID(menuItem.ID), win32ID)
}
if menuItem.Type == menu.RadioType {
// We need to maintain a list of this menu's radioitems
m.radioboxes = append(m.radioboxes, win32ID)
globalRadioGroupMap.addRadioGroupMapping(m.wailsMenu.Menu, wailsMenuItemID(menuItem.ID), win32ID)
if menuItem.Checked {
m.initiallySelectedRadioItems = append(m.initiallySelectedRadioItems, win32ID)
}
}
case menu.SeparatorType:
err := appendWin32MenuItem(parent, flags, 0, "")
if err != nil {
return err
}
}
return nil
}
func (m *Menu) processRadioGroups() error {
for _, rg := range applicationMenu.RadioGroups {
startWailsMenuID := wailsMenuItemID(rg.Members[0])
endWailsMenuID := wailsMenuItemID(rg.Members[len(rg.Members)-1])
startIDs := globalRadioGroupMap.getRadioGroupMapping(startWailsMenuID)
endIDs := globalRadioGroupMap.getRadioGroupMapping(endWailsMenuID)
var radioGroupMaps = []*radioGroupStartEnd{}
for index := range startIDs {
startID := startIDs[index]
endID := endIDs[index]
thisRadioGroup := &radioGroupStartEnd{
startID: startID,
endID: endID,
}
radioGroupMaps = append(radioGroupMaps, thisRadioGroup)
}
// Set this for each member
for _, member := range rg.Members {
id := wailsMenuItemID(member)
globalRadioGroupCache.addToRadioGroupCache(m.wailsMenu.Menu, id, radioGroupMaps)
}
}
// Enable all initially checked radio items
for _, win32MenuID := range m.initiallySelectedRadioItems {
menuItemDetails := getMenuCacheEntry(win32MenuID)
wailsMenuID := wailsMenuItemID(menuItemDetails.item.ID)
err := selectRadioItemFromWailsMenuID(wailsMenuID, win32MenuID)
if err != nil {
return err
}
}
return nil
}
func (m *Menu) Destroy() error {
// Release the MenuIDs
releaseMenuIDsForProcessedMenu(m.wailsMenu.Menu)
// Unload this menu's checkboxes from the cache
globalCheckboxCache.removeMenuFromCheckboxCache(m.wailsMenu.Menu)
// Unload this menu's radio groups from the cache
globalRadioGroupCache.removeMenuFromRadioBoxCache(m.wailsMenu.Menu)
globalRadioGroupMap.removeMenuFromRadioGroupMapping(m.wailsMenu.Menu)
// Delete menu
return destroyWin32Menu(m.menu)
}
var flagMap = map[menu.Type]uint32{
menu.TextType: MF_STRING,
menu.SeparatorType: MF_SEPARATOR,
menu.SubmenuType: MF_STRING | MF_POPUP,
menu.CheckboxType: MF_STRING,
menu.RadioType: MF_STRING,
}
func calculateFlags(menuItem *menumanager.ProcessedMenuItem) uint32 {
result := flagMap[menuItem.Type]
if menuItem.Disabled {
result |= MF_DISABLED
}
if menuItem.Type == menu.CheckboxType && menuItem.Checked {
result |= MF_CHECKED
}
return result
}

View File

@@ -0,0 +1,74 @@
//+build windows
package ffenestri
import (
"github.com/leaanthony/idgen"
"github.com/wailsapp/wails/v2/internal/menumanager"
"sync"
)
/**
MenuCache
---------
When windows calls back to Go (when an item is clicked), we need to
be able to retrieve information about the menu item:
- The menu that the menuitem is part of (parent)
- The original processed menu item
- The type of the menu (application, context or tray)
This cache is built up when a menu is created.
*/
// TODO: Make this like the other caches
type menuCacheEntry struct {
parent win32Menu
menuType menuType
item *menumanager.ProcessedMenuItem
processedMenu *menumanager.ProcessedMenu
}
var idGenerator = idgen.New()
var menuCache = map[win32MenuItemID]*menuCacheEntry{}
var menuCacheLock sync.RWMutex
var wailsMenuIDtoWin32IDMap = map[wailsMenuItemID]win32MenuItemID{}
// This releases the menuIDs back to the id generator
var winIDsOwnedByProcessedMenu = map[*menumanager.ProcessedMenu][]win32MenuItemID{}
func releaseMenuIDsForProcessedMenu(processedMenu *menumanager.ProcessedMenu) {
for _, menuID := range winIDsOwnedByProcessedMenu[processedMenu] {
idGenerator.ReleaseID(uint(menuID))
}
delete(winIDsOwnedByProcessedMenu, processedMenu)
}
func addMenuCacheEntry(parent win32Menu, typ menuType, wailsMenuItem *menumanager.ProcessedMenuItem, processedMenu *menumanager.ProcessedMenu) win32MenuItemID {
menuCacheLock.Lock()
defer menuCacheLock.Unlock()
id, err := idGenerator.NewID()
checkFatal(err)
menuID := win32MenuItemID(id)
menuCache[menuID] = &menuCacheEntry{
parent: parent,
menuType: typ,
item: wailsMenuItem,
processedMenu: processedMenu,
}
// save the mapping
wailsMenuIDtoWin32IDMap[wailsMenuItemID(wailsMenuItem.ID)] = menuID
// keep track of menuids owned by this menu (so we can release the ids)
winIDsOwnedByProcessedMenu[processedMenu] = append(winIDsOwnedByProcessedMenu[processedMenu], menuID)
return menuID
}
func getMenuCacheEntry(id win32MenuItemID) *menuCacheEntry {
menuCacheLock.Lock()
defer menuCacheLock.Unlock()
return menuCache[id]
}

View File

@@ -0,0 +1,194 @@
//+build windows
package ffenestri
import (
"fmt"
"github.com/leaanthony/slicer"
"os"
"sync"
"text/tabwriter"
"github.com/wailsapp/wails/v2/internal/menumanager"
)
/* ---------------------------------------------------------------------------------
Radio Groups
------------
Radio groups are stored by the ProcessedMenu as a list of menu ids.
Windows only cares about the start and end ids of the group so we
preprocess the radio groups and store this data in a radioGroupMap.
When a radio button is clicked, we use the menu id to read in the
radio group data and call CheckMenuRadioItem to update the group.
*/
type radioGroupStartEnd struct {
startID win32MenuItemID
endID win32MenuItemID
}
type RadioGroupCache struct {
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]*radioGroupStartEnd
mutex sync.RWMutex
}
func NewRadioGroupCache() *RadioGroupCache {
return &RadioGroupCache{
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]*radioGroupStartEnd),
}
}
func (c *RadioGroupCache) Dump() {
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
println("---------------- RadioGroupCache", c, "Dump ----------------")
for menu, processedMenu := range c.cache {
println("Menu", menu)
_, _ = fmt.Fprintf(w, "Wails ID \tWindows ID Pairs\n")
for wailsMenuItemID, radioGroupStartEnd := range processedMenu {
menus := slicer.String()
for _, se := range radioGroupStartEnd {
menus.Add(fmt.Sprintf("[%d -> %d]", se.startID, se.endID))
}
_, _ = fmt.Fprintf(w, "%s\t%s\n", wailsMenuItemID, menus.Join(", "))
_ = w.Flush()
}
}
}
func (c *RadioGroupCache) addToRadioGroupCache(menu *menumanager.ProcessedMenu, item wailsMenuItemID, radioGroupMaps []*radioGroupStartEnd) {
c.mutex.Lock()
// Get map for menu
if c.cache[menu] == nil {
c.cache[menu] = make(map[wailsMenuItemID][]*radioGroupStartEnd)
}
menuMap := c.cache[menu]
// Ensure we have a slice
if menuMap[item] == nil {
menuMap[item] = []*radioGroupStartEnd{}
}
menuMap[item] = radioGroupMaps
c.mutex.Unlock()
}
func (c *RadioGroupCache) removeMenuFromRadioBoxCache(menu *menumanager.ProcessedMenu) {
c.mutex.Lock()
delete(c.cache, menu)
c.mutex.Unlock()
}
func (c *RadioGroupCache) getRadioGroupMappings(wailsMenuID wailsMenuItemID) []*radioGroupStartEnd {
c.mutex.Lock()
result := []*radioGroupStartEnd{}
for _, menugroups := range c.cache {
groups := menugroups[wailsMenuID]
if groups != nil {
result = append(result, groups...)
}
}
c.mutex.Unlock()
return result
}
type RadioGroupMap struct {
cache map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID
mutex sync.RWMutex
}
func NewRadioGroupMap() *RadioGroupMap {
return &RadioGroupMap{
cache: make(map[*menumanager.ProcessedMenu]map[wailsMenuItemID][]win32MenuItemID),
}
}
func (c *RadioGroupMap) Dump() {
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
println("---------------- RadioGroupMap", c, "Dump ----------------")
for _, processedMenu := range c.cache {
_, _ = fmt.Fprintf(w, "Menu\tWails ID \tWindows IDs\n")
for wailsMenuItemID, win32menus := range processedMenu {
menus := slicer.String()
for _, win32menu := range win32menus {
menus.Add(fmt.Sprintf("%v", win32menu))
}
_, _ = fmt.Fprintf(w, "%p\t%s\t%s\n", processedMenu, wailsMenuItemID, menus.Join(", "))
_ = w.Flush()
}
}
}
func (m *RadioGroupMap) addRadioGroupMapping(menu *menumanager.ProcessedMenu, item wailsMenuItemID, win32ID win32MenuItemID) {
m.mutex.Lock()
// Get map for menu
if m.cache[menu] == nil {
m.cache[menu] = make(map[wailsMenuItemID][]win32MenuItemID)
}
menuMap := m.cache[menu]
// Ensure we have a slice
if menuMap[item] == nil {
menuMap[item] = []win32MenuItemID{}
}
menuMap[item] = append(menuMap[item], win32ID)
m.mutex.Unlock()
}
func (m *RadioGroupMap) removeMenuFromRadioGroupMapping(menu *menumanager.ProcessedMenu) {
m.mutex.Lock()
delete(m.cache, menu)
m.mutex.Unlock()
}
func (m *RadioGroupMap) getRadioGroupMapping(wailsMenuID wailsMenuItemID) []win32MenuItemID {
m.mutex.Lock()
result := []win32MenuItemID{}
for _, menuids := range m.cache {
ids := menuids[wailsMenuID]
if ids != nil {
result = append(result, ids...)
}
}
m.mutex.Unlock()
return result
}
func selectRadioItemFromWailsMenuID(wailsMenuID wailsMenuItemID, win32MenuID win32MenuItemID) error {
radioItemGroups := globalRadioGroupCache.getRadioGroupMappings(wailsMenuID)
// Figure out offset into group
var offset win32MenuItemID = 0
for _, radioItemGroup := range radioItemGroups {
if win32MenuID >= radioItemGroup.startID && win32MenuID <= radioItemGroup.endID {
offset = win32MenuID - radioItemGroup.startID
break
}
}
for _, radioItemGroup := range radioItemGroups {
selectedMenuID := radioItemGroup.startID + offset
menuItemDetails := getMenuCacheEntry(selectedMenuID)
if menuItemDetails != nil {
if menuItemDetails.parent != 0 {
err := selectRadioItem(selectedMenuID, radioItemGroup.startID, radioItemGroup.endID, menuItemDetails.parent)
if err != nil {
return err
}
}
}
}
return nil
}

View File

@@ -0,0 +1,129 @@
//+build windows
package ffenestri
import (
"unsafe"
"github.com/wailsapp/wails/v2/internal/menumanager"
"golang.org/x/sys/windows"
)
var (
// DLL stuff
user32 = windows.NewLazySystemDLL("User32.dll")
win32CreateMenu = user32.NewProc("CreateMenu")
win32DestroyMenu = user32.NewProc("DestroyMenu")
win32CreatePopupMenu = user32.NewProc("CreatePopupMenu")
win32AppendMenuW = user32.NewProc("AppendMenuW")
win32SetMenu = user32.NewProc("SetMenu")
win32CheckMenuItem = user32.NewProc("CheckMenuItem")
win32GetMenuState = user32.NewProc("GetMenuState")
win32CheckMenuRadioItem = user32.NewProc("CheckMenuRadioItem")
applicationMenu *menumanager.WailsMenu
menuManager *menumanager.Manager
)
const MF_BITMAP uint32 = 0x00000004
const MF_CHECKED uint32 = 0x00000008
const MF_DISABLED uint32 = 0x00000002
const MF_ENABLED uint32 = 0x00000000
const MF_GRAYED uint32 = 0x00000001
const MF_MENUBARBREAK uint32 = 0x00000020
const MF_MENUBREAK uint32 = 0x00000040
const MF_OWNERDRAW uint32 = 0x00000100
const MF_POPUP uint32 = 0x00000010
const MF_SEPARATOR uint32 = 0x00000800
const MF_STRING uint32 = 0x00000000
const MF_UNCHECKED uint32 = 0x00000000
const MF_BYCOMMAND uint32 = 0x00000000
const MF_BYPOSITION uint32 = 0x00000400
const WM_SIZE = 5
const WM_GETMINMAXINFO = 36
type Win32Rect struct {
Left int32
Top int32
Right int32
Bottom int32
}
// ------------------- win32 calls -----------------------
func createWin32Menu() (win32Menu, error) {
res, _, err := win32CreateMenu.Call()
if res == 0 {
return 0, wall(err)
}
return win32Menu(res), nil
}
func destroyWin32Menu(menu win32Menu) error {
res, _, err := win32DestroyMenu.Call(uintptr(menu))
if res == 0 {
return wall(err, "Menu:", menu)
}
return nil
}
func createWin32PopupMenu() (win32Menu, error) {
res, _, err := win32CreatePopupMenu.Call()
if res == 0 {
return 0, wall(err)
}
return win32Menu(res), nil
}
func appendWin32MenuItem(menu win32Menu, flags uintptr, submenuOrID uintptr, label string) error {
menuText, err := windows.UTF16PtrFromString(label)
if err != nil {
return err
}
res, _, err := win32AppendMenuW.Call(
uintptr(menu),
flags,
submenuOrID,
uintptr(unsafe.Pointer(menuText)),
)
if res == 0 {
return wall(err, "Menu", menu, "Flags", flags, "submenuOrID", submenuOrID, "label", label)
}
return nil
}
func setWindowMenu(window win32Window, menu win32Menu) error {
res, _, err := win32SetMenu.Call(uintptr(window), uintptr(menu))
if res == 0 {
return wall(err, "window", window, "menu", menu)
}
return nil
}
func selectRadioItem(selectedMenuID, startMenuItemID, endMenuItemID win32MenuItemID, parent win32Menu) error {
res, _, err := win32CheckMenuRadioItem.Call(uintptr(parent), uintptr(startMenuItemID), uintptr(endMenuItemID), uintptr(selectedMenuID), uintptr(MF_BYCOMMAND))
if int(res) == 0 {
return wall(err, selectedMenuID, startMenuItemID, endMenuItemID, parent)
}
return nil
}
//
//func getWindowRect(window win32Window) (*Win32Rect, error) {
// var windowRect Win32Rect
// res, _, err := win32GetWindowRect.Call(uintptr(window), uintptr(unsafe.Pointer(&windowRect)))
// if res == 0 {
// return nil, err
// }
// return &windowRect, nil
//}
//
//func getClientRect(window win32Window) (*Win32Rect, error) {
// var clientRect Win32Rect
// res, _, err := win32GetClientRect.Call(uintptr(window), uintptr(unsafe.Pointer(&clientRect)))
// if res == 0 {
// return nil, err
// }
// return &clientRect, nil
//}
//

View File

@@ -12,7 +12,8 @@ class wv2ComHandler
: public ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler,
public ICoreWebView2CreateCoreWebView2ControllerCompletedHandler,
public ICoreWebView2WebMessageReceivedEventHandler,
public ICoreWebView2PermissionRequestedEventHandler
public ICoreWebView2PermissionRequestedEventHandler,
public ICoreWebView2AcceleratorKeyPressedEventHandler
{
struct Application *app;
@@ -44,6 +45,7 @@ class wv2ComHandler
ICoreWebView2 *webview;
::EventRegistrationToken token;
controller->get_CoreWebView2(&webview);
controller->add_AcceleratorKeyPressed(this, &token);
webview->add_WebMessageReceived(this, &token);
webview->add_PermissionRequested(this, &token);
@@ -51,6 +53,39 @@ class wv2ComHandler
return S_OK;
}
// This is our keyboard callback method
HRESULT STDMETHODCALLTYPE Invoke(ICoreWebView2Controller *controller, ICoreWebView2AcceleratorKeyPressedEventArgs * args) {
COREWEBVIEW2_KEY_EVENT_KIND kind;
args->get_KeyEventKind(&kind);
if (kind == COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN ||
kind == COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN)
{
// UINT key;
// args->get_VirtualKey(&key);
// printf("Got key: %d\n", key);
args->put_Handled(TRUE);
// Check if the key is one we want to handle.
// if (std::function<void()> action =
// m_appWindow->GetAcceleratorKeyFunction(key))
// {
// // Keep the browser from handling this key, whether it's autorepeated or
// // not.
// CHECK_FAILURE(args->put_Handled(TRUE));
//
// // Filter out autorepeated keys.
// COREWEBVIEW2_PHYSICAL_KEY_STATUS status;
// CHECK_FAILURE(args->get_PhysicalKeyStatus(&status));
// if (!status.WasKeyDown)
// {
// // Perform the action asynchronously to avoid blocking the
// // browser process's event queue.
// m_appWindow->RunAsync(action);
// }
// }
}
return S_OK;
}
// This is called when JS posts a message back to webkit
HRESULT STDMETHODCALLTYPE Invoke(
ICoreWebView2 *sender, ICoreWebView2WebMessageReceivedEventArgs *args) {

View File

@@ -338,3 +338,60 @@ func CopyDirExtended(src string, dst string, ignore []string) (err error) {
return
}
// MoveDirExtended recursively moves a directory tree, attempting to preserve permissions.
// Source directory must exist, destination directory must *not* exist. It ignores any files or
// directories that are given through the ignore parameter.
// Symlinks are ignored and skipped.
func MoveDirExtended(src string, dst string, ignore []string) (err error) {
ignoreList := slicer.String(ignore)
src = filepath.Clean(src)
dst = filepath.Clean(dst)
si, err := os.Stat(src)
if err != nil {
return err
}
if !si.IsDir() {
return fmt.Errorf("source is not a directory")
}
_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
return
}
if err == nil {
return fmt.Errorf("destination already exists")
}
err = MkDirs(dst)
if err != nil {
return
}
entries, err := ioutil.ReadDir(src)
if err != nil {
return
}
for _, entry := range entries {
if ignoreList.Contains(entry.Name()) {
continue
}
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
// Skip symlinks.
if entry.Mode()&os.ModeSymlink != 0 {
continue
}
err := os.Rename(srcPath, dstPath)
if err != nil {
return err
}
}
return
}

View File

@@ -23,6 +23,10 @@ func (m *Manager) GetApplicationMenuJSON() string {
return m.applicationMenuJSON
}
func (m *Manager) GetProcessedApplicationMenu() *WailsMenu {
return m.processedApplicationMenu
}
// UpdateApplicationMenu reprocesses the application menu to pick up structure
// changes etc
// Returns the JSON representation of the updated menu
@@ -36,8 +40,9 @@ func (m *Manager) UpdateApplicationMenu() (string, error) {
func (m *Manager) processApplicationMenu() error {
// Process the menu
processedApplicationMenu := NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu)
applicationMenuJSON, err := processedApplicationMenu.AsJSON()
m.processedApplicationMenu = NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu)
m.processRadioGroups(m.processedApplicationMenu, m.applicationMenuItemMap)
applicationMenuJSON, err := m.processedApplicationMenu.AsJSON()
if err != nil {
return err
}

View File

@@ -8,8 +8,9 @@ import (
type Manager struct {
// The application menu.
applicationMenu *menu.Menu
applicationMenuJSON string
applicationMenu *menu.Menu
applicationMenuJSON string
processedApplicationMenu *WailsMenu
// Our application menu mappings
applicationMenuItemMap *MenuItemMap
@@ -21,6 +22,9 @@ type Manager struct {
// Tray menu stores
trayMenus map[string]*TrayMenu
trayMenuPointers map[*menu.TrayMenu]string
// Radio groups
radioGroups map[*menu.MenuItem][]*menu.MenuItem
}
func NewManager() *Manager {
@@ -30,6 +34,7 @@ func NewManager() *Manager {
contextMenuPointers: make(map[*menu.ContextMenu]string),
trayMenus: make(map[string]*TrayMenu),
trayMenuPointers: make(map[*menu.TrayMenu]string),
radioGroups: make(map[*menu.MenuItem][]*menu.MenuItem),
}
}
@@ -72,6 +77,14 @@ func (m *Manager) ProcessClick(menuID string, data string, menuType string, pare
menuItem.Checked = !menuItem.Checked
}
if menuItem.Type == menu.RadioType {
println("Toggle radio")
// Get my radio group
for _, radioMenuItem := range m.radioGroups[menuItem] {
radioMenuItem.Checked = (radioMenuItem == menuItem)
}
}
if menuItem.Click == nil {
// No callback
return fmt.Errorf("No callback for menu '%s'", menuItem.Label)
@@ -88,3 +101,16 @@ func (m *Manager) ProcessClick(menuID string, data string, menuType string, pare
return nil
}
func (m *Manager) processRadioGroups(processedMenu *WailsMenu, itemMap *MenuItemMap) {
for _, group := range processedMenu.RadioGroups {
radioGroupMenuItems := []*menu.MenuItem{}
for _, member := range group.Members {
item := m.getMenuItemByID(itemMap, member)
radioGroupMenuItems = append(radioGroupMenuItems, item)
}
for _, radioGroupMenuItem := range radioGroupMenuItems {
m.radioGroups[radioGroupMenuItem] = radioGroupMenuItems
}
}
}

View File

@@ -5,6 +5,7 @@ package system
import (
"os/exec"
"strings"
"syscall"
"github.com/wailsapp/wails/v2/internal/system/packagemanager"
@@ -44,3 +45,14 @@ func (i *Info) discover() error {
i.Dependencies = append(i.Dependencies, checkUPX())
return nil
}
// IsAppleSilicon returns true if the app is running on Apple Silicon
// Credit: https://www.yellowduck.be/posts/detecting-apple-silicon-via-go/
func IsAppleSilicon() bool {
r, err := syscall.Sysctl("sysctl.proc_translated")
if err != nil {
return false
}
return r == "\x00\x00\x00" || r == "\x01\x00\x00"
}

View File

@@ -27,3 +27,10 @@ func (i *Info) discover() error {
return nil
}
// IsAppleSilicon returns true if the app is running on Apple Silicon
// Credit: https://www.yellowduck.be/posts/detecting-apple-silicon-via-go/
// NOTE: Not applicable to linux
func IsAppleSilicon() bool {
return false
}

View File

@@ -43,3 +43,10 @@ func (i *Info) discover() error {
return nil
}
// IsAppleSilicon returns true if the app is running on Apple Silicon
// Credit: https://www.yellowduck.be/posts/detecting-apple-silicon-via-go/
// NOTE: Not applicable to windows
func IsAppleSilicon() bool {
return false
}

View File

@@ -1,7 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="MyApplication" version="1.0.0.0" processorArchitecture="amd64"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->

View File

@@ -171,6 +171,9 @@ func (b *BaseBuilder) OutputFilename(options *Options) string {
case "windows":
outputFile = target + ".exe"
case "darwin", "linux":
if b.options.Arch == "" {
b.options.Arch = runtime.GOARCH
}
outputFile = fmt.Sprintf("%s-%s-%s", target, b.options.Platform, b.options.Arch)
}

View File

@@ -48,6 +48,7 @@ type Options struct {
CompressFlags string // Flags to pass to UPX
AppleIdentity string
WebView2Strategy string // WebView2 installer strategy
RunDelve bool // Indicates if we should run delve after the build
}
// Build the project!

11
v2/pkg/menu/windows.go Normal file
View File

@@ -0,0 +1,11 @@
package menu
// DefaultWindowsMenu returns a default menu including the default
// Application and Edit menus. Use `.Append()` to add to it.
func DefaultWindowsMenu() *Menu {
return NewMenuFromItems(
FileMenu(),
EditMenu(),
WindowMenu(),
)
}

View File

@@ -107,14 +107,14 @@ func GetApplicationMenu(appoptions *App) *menu.Menu {
if appoptions.Mac != nil {
result = appoptions.Mac.Menu
}
//case "linux":
// if appoptions.Linux != nil {
// result = appoptions.Linux.TrayMenu
// }
//case "windows":
// if appoptions.Windows != nil {
// result = appoptions.Windows.TrayMenu
// }
//case "linux":
// if appoptions.Linux != nil {
// result = appoptions.Linux.TrayMenu
// }
case "windows":
if appoptions.Windows != nil {
result = appoptions.Windows.Menu
}
}
if result == nil {

View File

@@ -1,8 +1,11 @@
package windows
// Options are options specific to Mac
import "github.com/wailsapp/wails/v2/pkg/menu"
// Options are options specific to Windows
type Options struct {
WebviewIsTransparent bool
WindowBackgroundIsTranslucent bool
DisableWindowIcon bool
Menu *menu.Menu
}