Compare commits

...

39 Commits

Author SHA1 Message Date
Lea Anthony
d3501f4cb7 v2.0.0-alpha.19 2021-01-31 21:59:16 +11:00
Lea Anthony
ee82cd25b7 Menu Items default to 12 pt on Mac 2021-01-31 21:58:41 +11:00
Lea Anthony
bbb07e17d9 v2.0.0-alpha.18 2021-01-31 21:20:40 +11:00
Lea Anthony
e6b40b55c4 Add nil guard for binding 2021-01-31 21:10:30 +11:00
Lea Anthony
7573f68df3 Add argument guard for methods 2021-01-31 15:35:33 +11:00
Lea Anthony
ceaacc7ba9 Re-added Store.Set() using deep copy 2021-01-30 21:36:59 +11:00
Lea Anthony
0e24f75753 Deep copy initial value in store 2021-01-30 21:29:36 +11:00
Lea Anthony
82b9deeee4 Unexport set 2021-01-30 20:47:10 +11:00
Lea Anthony
cfa40b797f More race condition fixes 2021-01-29 13:03:05 +11:00
Lea Anthony
5aeb68acb7 Fix for calculating new window size 2021-01-28 19:26:26 +11:00
Lea Anthony
b81101414f Support Min/Max Size in Window runtime 2021-01-28 18:48:07 +11:00
Lea Anthony
7ae89d04bb Fixed data race in store. Handle nils better 2021-01-28 07:15:29 +11:00
Lea Anthony
1c566f3802 Fixed data race in servicebus 2021-01-28 06:23:49 +11:00
Lea Anthony
c9c3c9ab90 Don't bind startup/shutdown methods 2021-01-27 21:12:17 +11:00
Lea Anthony
56394ac50e Use mutex when doing a Store resync 2021-01-27 06:03:17 +11:00
Lea Anthony
f7c2f12ab2 Added error handler for dealing with loading user js code 2021-01-26 16:01:06 +11:00
Lea Anthony
a6bb6e0c93 v2.0.0-alpha.17 2021-01-26 12:30:45 +11:00
Lea Anthony
4a5863e6ac Ported update command 2021-01-26 12:30:14 +11:00
Lea Anthony
913fe8d184 v2.0.0-alpha.16 2021-01-26 11:11:23 +11:00
Lea Anthony
4ce8130cdf Replace CreateApp with single Run() method 2021-01-26 11:09:17 +11:00
Lea Anthony
fe87463b78 Move Bind() into app config 2021-01-26 07:04:12 +11:00
Lea Anthony
fe0f0e29e8 Update CreateApp API 2021-01-26 06:45:23 +11:00
Lea Anthony
83d6dac7cf Add appType to builds. Update server code to compile 2021-01-26 06:40:41 +11:00
Lea Anthony
02500e0930 Update vanilla template with new API 2021-01-26 06:40:18 +11:00
Lea Anthony
5e1187f437 Startup/Shutdown hook warnings 2021-01-26 06:39:54 +11:00
Lea Anthony
064ff3b65e Change build wording 2021-01-26 06:38:54 +11:00
Lea Anthony
b5c7019bf0 Fix compile warnings 2021-01-26 06:37:33 +11:00
Lea Anthony
e9d16e77a3 Add support for loglevel flag in debug builds 2021-01-25 21:42:31 +11:00
Lea Anthony
2415d4c531 v2.0.0-alpha.15 2021-01-25 21:21:44 +11:00
Lea Anthony
3f75213ce3 Small linting fixes 2021-01-25 21:05:20 +11:00
Lea Anthony
6120ceabf1 Support Fonts & Colours for menuitems 2021-01-25 21:04:57 +11:00
Lea Anthony
95a95d1750 Ensure store does initial resync 2021-01-25 21:02:36 +11:00
Lea Anthony
d923e84456 v2.0.0-alpha.14 2021-01-23 21:00:04 +11:00
Lea Anthony
343f573e78 Fix menu tooltips 2021-01-23 20:59:02 +11:00
Lea Anthony
c6d87da4f0 v2.0.0-alpha.13 2021-01-23 20:39:43 +11:00
Lea Anthony
a9faebe51a MenuItem image support 2021-01-23 20:32:42 +11:00
Lea Anthony
d436f5d1be v2.0.0-alpha.12 2021-01-23 16:22:00 +11:00
Lea Anthony
f40899821f Support ToolTips 2021-01-23 16:14:48 +11:00
Lea Anthony
2a64ed19a3 v2.0.0-alpha.11 2021-01-22 14:57:50 +11:00
50 changed files with 2284 additions and 153 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ package main
import (
"os"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/update"
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/debug"
@@ -48,6 +50,11 @@ func main() {
fatal(err.Error())
}
err = update.AddSubcommand(app, os.Stdout, version)
if err != nil {
fatal(err.Error())
}
err = app.Run()
if err != nil {
println("\n\nERROR: " + err.Error())

View File

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

View File

@@ -3,6 +3,7 @@ module github.com/wailsapp/wails/v2
go 1.15
require (
github.com/Masterminds/semver v1.5.0
github.com/davecgh/go-spew v1.1.1
github.com/fatih/structtag v1.2.0
github.com/fsnotify/fsnotify v1.4.9

View File

@@ -1,3 +1,5 @@
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

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

View File

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

View File

@@ -17,6 +17,8 @@ import (
// App defines a Wails application structure
type App struct {
appType string
window *ffenestri.Application
servicebus *servicebus.ServiceBus
logger *logger.Logger
@@ -79,11 +81,15 @@ func CreateApp(appoptions *options.App) (*App, error) {
window := ffenestri.NewApplicationWithConfig(appoptions, myLogger, menuManager)
// Create binding exemptions - Ugly hack. There must be a better way
bindingExemptions := []interface{}{appoptions.Startup, appoptions.Shutdown}
result := &App{
appType: "desktop",
window: window,
servicebus: servicebus.New(myLogger),
logger: myLogger,
bindings: binding.NewBindings(myLogger),
bindings: binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions),
menuManager: menuManager,
startupCallback: appoptions.Startup,
shutdownCallback: appoptions.Shutdown,
@@ -213,14 +219,3 @@ func (a *App) Run() error {
return result
}
// Bind a struct to the application by passing in
// a pointer to it
func (a *App) Bind(structPtr interface{}) {
// Add the struct to the bindings
err := a.bindings.Add(structPtr)
if err != nil {
a.logger.Fatal("Error during binding: " + err.Error())
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -140,6 +140,16 @@ void Debug(struct Application *app, const char *message, ... ) {
}
}
void Error(struct Application *app, const char *message, ... ) {
const char *temp = concat("LEFfenestri (C) | ", message);
va_list args;
va_start(args, message);
vsnprintf(logbuffer, MAXMESSAGE, temp, args);
app->sendMessageToBackend(&logbuffer[0]);
MEMFREE(temp);
va_end(args);
}
void Fatal(struct Application *app, const char *message, ... ) {
const char *temp = concat("LFFfenestri (C) | ", message);
va_list args;
@@ -150,6 +160,12 @@ void Fatal(struct Application *app, const char *message, ... ) {
va_end(args);
}
// Requires NSString input EG lookupStringConstant(str("NSFontAttributeName"))
void* lookupStringConstant(id constantName) {
void ** dataPtr = CFBundleGetDataPointerForName(CFBundleGetBundleWithIdentifier((CFStringRef)str("com.apple.AppKit")), (CFStringRef) constantName);
return (dataPtr ? *dataPtr : nil);
}
bool isRetina(struct Application *app) {
CGFloat scale = GET_BACKINGSCALEFACTOR(app->mainWindow);
if( (int)scale == 1 ) {
@@ -240,7 +256,10 @@ void messageHandler(id self, SEL cmd, id contentController, id message) {
struct Application *app = (struct Application *)objc_getAssociatedObject(
self, "application");
const char *name = (const char *)msg(msg(message, s("name")), s("UTF8String"));
if( strcmp(name, "completed") == 0) {
if( strcmp(name, "error") == 0 ) {
printf("There was a Javascript error. Please open the devtools for more information.\n");
Show(app);
} else if( strcmp(name, "completed") == 0) {
// Delete handler
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("completed"));
@@ -444,6 +463,7 @@ void DestroyApplication(struct Application *app) {
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("contextMenu"));
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("windowDrag"));
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("external"));
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("error"));
// Close main window
msg(app->mainWindow, s("close"));
@@ -879,6 +899,20 @@ void setMinMaxSize(struct Application *app)
{
msg(app->mainWindow, s("setMinSize:"), CGSizeMake(app->minWidth, app->minHeight));
}
// Calculate if window needs resizing
int newWidth = app->width;
int newHeight = app->height;
if (app->maxWidth > 0 && app->width > app->maxWidth) newWidth = app->maxWidth;
if (app->minWidth > 0 && app->width < app->minWidth) newWidth = app->minWidth;
if (app->maxHeight > 0 && app->height > app->maxHeight ) newHeight = app->maxHeight;
if (app->minHeight > 0 && app->height < app->minHeight ) newHeight = app->minHeight;
// If we have any change, resize window
if ( newWidth != app->width || newHeight != app->height ) {
SetSize(app, newWidth, newHeight);
}
}
void SetMinWindowSize(struct Application *app, int minWidth, int minHeight)
@@ -1528,6 +1562,7 @@ void Run(struct Application *app, int argc, char **argv) {
id manager = msg(config, s("userContentController"));
msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("external"));
msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("completed"));
msg(manager, s("addScriptMessageHandler:name:"), app->delegate, str("error"));
app->manager = manager;
id wkwebview = msg(c("WKWebView"), s("alloc"));

View File

@@ -2,7 +2,7 @@ package ffenestri
/*
#cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1
#cgo darwin LDFLAGS: -framework WebKit -lobjc
#cgo darwin LDFLAGS: -framework WebKit -framework CoreFoundation -lobjc
#include "ffenestri.h"
#include "ffenestri_darwin.h"

View File

@@ -6,6 +6,7 @@
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#include <objc/objc-runtime.h>
#include <CoreGraphics/CoreGraphics.h>
#include <CoreFoundation/CoreFoundation.h>
#include "json.h"
#include "hashmap.h"
#include "stdlib.h"
@@ -20,7 +21,6 @@
#define strunicode(input) msg(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg(input, s("UTF8String"))
#define url(input) msg(c("NSURL"), s("fileURLWithPath:"), str(input))
#define ALLOC(classname) msg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init"))
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame"))
@@ -110,4 +110,6 @@ void SetTray(struct Application* app, const char *, const char *, const char *);
//void SetContextMenus(struct Application* app, const char *);
void AddTrayMenu(struct Application* app, const char *);
void* lookupStringConstant(id constantName);
#endif

View File

@@ -5,6 +5,7 @@
#include "ffenestri_darwin.h"
#include "menu_darwin.h"
#include "contextmenus_darwin.h"
#include "common.h"
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData) {
@@ -63,8 +64,6 @@ MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, id menuItem, const
return result;
}
void DeleteMenu(Menu *menu) {
// Free menu item hashmap
@@ -576,7 +575,7 @@ id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const c
return item;
}
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers) {
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA) {
id item = ALLOC("NSMenuItem");
// Create a MenuItemCallbackData
@@ -589,6 +588,73 @@ id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuItemCallback:"), key);
if( tooltip != NULL ) {
msg(item, s("setToolTip:"), str(tooltip));
}
// Process image
if( image != NULL && strlen(image) > 0) {
id data = ALLOC("NSData");
id imageData = msg(data, s("initWithBase64EncodedString:options:"), str(image), 0);
id nsimage = ALLOC("NSImage");
msg(nsimage, s("initWithData:"), imageData);
msg(item, s("setImage:"), nsimage);
}
// Process Menu Item attributes
id dictionary = ALLOC_INIT("NSMutableDictionary");
// Process font
id font;
CGFloat fontSizeFloat = (CGFloat)fontSize;
// Check if valid
id fontNameAsNSString = str(fontName);
id fontsOnSystem = msg(msg(c("NSFontManager"), s("sharedFontManager")), s("availableFonts"));
bool valid = msg(fontsOnSystem, s("containsObject:"), fontNameAsNSString);
if( valid ) {
font = msg(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
} else {
bool supportsMonospacedDigitSystemFont = (bool) msg(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:"));
if( supportsMonospacedDigitSystemFont ) {
font = msg(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, NSFontWeightRegular);
} else {
font = msg(c("NSFont"), s("menuFontOfSize:"), fontSizeFloat);
}
}
// Add font to dictionary
msg(dictionary, s("setObject:forKey:"), font, lookupStringConstant(str("NSFontAttributeName")));
// Add offset to dictionary
id offset = msg(c("NSNumber"), s("numberWithFloat:"), 0.0);
msg(dictionary, s("setObject:forKey:"), offset, lookupStringConstant(str("NSBaselineOffsetAttributeName")));
// RGBA
if( RGBA != NULL && strlen(RGBA) > 0) {
unsigned short r, g, b, a;
// white by default
r = g = b = a = 255;
int count = sscanf(RGBA, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a);
if (count > 0) {
id colour = msg(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"),
(float)r / 255.0,
(float)g / 255.0,
(float)b / 255.0,
(float)a / 255.0);
msg(dictionary, s("setObject:forKey:"), colour, lookupStringConstant(str("NSForegroundColorAttributeName")));
msg(colour, s("release"));
}
}
id attributedString = ALLOC("NSMutableAttributedString");
msg(attributedString, s("initWithString:attributes:"), str(title), dictionary);
msg(dictionary, s("release"));
msg(item, s("setAttributedTitle:"), attributedString);
msg(attributedString, s("autorelease"));
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
@@ -670,6 +736,13 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
const char *acceleratorkey = NULL;
const char **modifiers = NULL;
const char *tooltip = getJSONString(item, "Tooltip");
const char *image = getJSONString(item, "Image");
const char *fontName = getJSONString(item, "FontName");
const char *RGBA = getJSONString(item, "RGBA");
int fontSize = 12;
getJSONInt(item, "FontSize", &fontSize);
// If we have an accelerator
if( accelerator != NULL ) {
// Get the key
@@ -702,7 +775,7 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
if( type != NULL ) {
if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers);
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA);
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);

View File

@@ -14,6 +14,21 @@ static const char *MenuTypeAsString[] = {
"ApplicationMenu", "ContextMenu", "TrayMenu",
};
typedef struct _NSRange {
unsigned long location;
unsigned long length;
} NSRange;
#define NSFontWeightUltraLight -0.8
#define NSFontWeightThin -0.6
#define NSFontWeightLight -0.4
#define NSFontWeightRegular 0
#define NSFontWeightMedium 0.23
#define NSFontWeightSemibold 0.3
#define NSFontWeightBold 0.4
#define NSFontWeightHeavy 0.56
#define NSFontWeightBlack 0.62
extern void messageFromWindowCallback(const char *);
typedef struct {
@@ -90,8 +105,7 @@ id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData);

File diff suppressed because one or more lines are too long

View File

@@ -97,14 +97,13 @@ void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON);
DumpTrayMenu(newMenu);
// DumpTrayMenu(newMenu);
// Get the current menu
TrayMenu *currentMenu = GetTrayMenuFromStore(store, newMenu->ID);
// If we don't have a menu, we create one
if ( currentMenu == NULL ) {
printf(" currentMenu = NULL\n");
// Store the new menu
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
@@ -112,7 +111,7 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
ShowTrayMenu(newMenu);
return;
}
DumpTrayMenu(currentMenu);
// DumpTrayMenu(currentMenu);
// Save the status bar reference
newMenu->statusbaritem = currentMenu->statusbaritem;

View File

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

View File

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

View File

@@ -9,28 +9,35 @@ import (
type ProcessedMenuItem struct {
ID string
// Label is what appears as the menu text
Label string
Label string `json:",omitempty"`
// Role is a predefined menu type
Role menu.Role `json:"Role,omitempty"`
Role menu.Role `json:",omitempty"`
// Accelerator holds a representation of a key binding
Accelerator *keys.Accelerator `json:"Accelerator,omitempty"`
Accelerator *keys.Accelerator `json:",omitempty"`
// Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu
Type menu.Type
// Disabled makes the item unselectable
Disabled bool
Disabled bool `json:",omitempty"`
// Hidden ensures that the item is not shown in the menu
Hidden bool
Hidden bool `json:",omitempty"`
// Checked indicates if the item is selected (used by Checkbox and Radio types only)
Checked bool
Checked bool `json:",omitempty"`
// Submenu contains a list of menu items that will be shown as a submenu
//SubMenu []*MenuItem `json:"SubMenu,omitempty"`
SubMenu *ProcessedMenu `json:"SubMenu,omitempty"`
SubMenu *ProcessedMenu `json:",omitempty"`
// Foreground colour in hex RGBA format EG: 0xFF0000FF = #FF0000FF = red
Foreground int
// Colour
RGBA string `json:",omitempty"`
// Background colour
Background int
// Font
FontSize int `json:",omitempty"`
FontName string `json:",omitempty"`
// Image - base64 image data
Image string `json:",omitempty"`
// Tooltip
Tooltip string `json:",omitempty"`
}
func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem {
@@ -45,8 +52,12 @@ func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *Pr
Disabled: menuItem.Disabled,
Hidden: menuItem.Hidden,
Checked: menuItem.Checked,
Foreground: menuItem.Foreground,
Background: menuItem.Background,
SubMenu: nil,
RGBA: menuItem.RGBA,
FontSize: menuItem.FontSize,
FontName: menuItem.FontName,
Image: menuItem.Image,
Tooltip: menuItem.Tooltip,
}
if menuItem.SubMenu != nil {

View File

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

View File

@@ -2,11 +2,12 @@ package messagedispatcher
import (
"encoding/json"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"strconv"
"strings"
"sync"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"github.com/wailsapp/wails/v2/internal/crypto"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
@@ -349,6 +350,38 @@ func (d *Dispatcher) processWindowMessage(result *servicebus.Message) {
for _, client := range d.clients {
client.frontend.WindowSize(w, h)
}
case "minsize":
// We need 2 arguments
if len(splitTopic) != 4 {
d.logger.Error("Invalid number of parameters for 'window:minsize' : %#v", result.Data())
return
}
w, err1 := strconv.Atoi(splitTopic[2])
h, err2 := strconv.Atoi(splitTopic[3])
if err1 != nil || err2 != nil {
d.logger.Error("Invalid integer parameters for 'window:minsize' : %#v", result.Data())
return
}
// Notifh clients
for _, client := range d.clients {
client.frontend.WindowSetMinSize(w, h)
}
case "maxsize":
// We need 2 arguments
if len(splitTopic) != 4 {
d.logger.Error("Invalid number of parameters for 'window:maxsize' : %#v", result.Data())
return
}
w, err1 := strconv.Atoi(splitTopic[2])
h, err2 := strconv.Atoi(splitTopic[3])
if err1 != nil || err2 != nil {
d.logger.Error("Invalid integer parameters for 'window:maxsize' : %#v", result.Data())
return
}
// Notifh clients
for _, client := range d.clients {
client.frontend.WindowSetMaxSize(w, h)
}
default:
d.logger.Error("Unknown window command: %s", command)
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,13 @@ package main
import (
"bytes"
"fmt"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/shell"
"io/ioutil"
"log"
"os"
"strings"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/shell"
)
func main() {
@@ -49,7 +50,7 @@ func main() {
log.Fatal(err)
}
wailsJS := fs.RelativePath("../../../internal/runtime/assets/desktop.js")
wailsJS := fs.RelativePath("../../../internal/runtime/assets/desktop_" + platform + ".js")
runtimeData, err := ioutil.ReadFile(wailsJS)
if err != nil {
log.Fatal(err)

View File

@@ -6,9 +6,12 @@ import (
"bytes"
"encoding/json"
"fmt"
golog "log"
"os"
"reflect"
"sync"
"github.com/wailsapp/wails/v2/internal/deepcopy"
)
// Options defines the optional data that may be used
@@ -64,21 +67,31 @@ func fatal(err error) {
// New creates a new store
func (p *StoreProvider) New(name string, defaultValue interface{}) *Store {
dataType := reflect.TypeOf(defaultValue)
if defaultValue == nil {
golog.Fatal("Cannot initialise a store with nil")
}
result := Store{
name: name,
runtime: p.runtime,
data: reflect.ValueOf(defaultValue),
dataType: dataType,
name: name,
runtime: p.runtime,
}
// Setup the sync listener
result.setupListener()
result.Set(defaultValue)
return &result
}
func (s *Store) lock() {
s.mux.Lock()
}
func (s *Store) unlock() {
s.mux.Unlock()
}
// OnError takes a function that will be called
// whenever an error occurs
func (s *Store) OnError(callback func(error)) {
@@ -105,7 +118,7 @@ func (s *Store) processUpdatedData(data string) error {
}
// Lock mutex for writing
s.mux.Lock()
s.lock()
// Handle nulls
if newData == nil {
@@ -116,7 +129,7 @@ func (s *Store) processUpdatedData(data string) error {
}
// Unlock mutex
s.mux.Unlock()
s.unlock()
return nil
}
@@ -146,20 +159,34 @@ func (s *Store) setupListener() {
// Resetting the curent data will resync
s.resync()
})
// Do initial resync
s.resync()
}
func (s *Store) resync() {
// Stringify data
newdata, err := json.Marshal(s.data.Interface())
if err != nil {
if s.errorHandler != nil {
s.errorHandler(err)
return
// Lock
s.lock()
defer s.unlock()
var result string
if s.data.IsValid() {
rawdata, err := json.Marshal(s.data.Interface())
if err != nil {
if s.errorHandler != nil {
s.errorHandler(err)
return
}
}
result = string(rawdata)
} else {
result = "{}"
}
// Emit event to front end
s.runtime.Events.Emit("wails:sync:store:updatedbybackend:"+s.name, string(newdata))
s.runtime.Events.Emit("wails:sync:store:updatedbybackend:"+s.name, result)
// Notify subscribers
s.notify()
@@ -172,7 +199,9 @@ func (s *Store) notify() {
for _, callback := range s.callbacks {
// Build args
s.lock()
args := []reflect.Value{s.data}
s.unlock()
if s.notifySynchronously {
callback.Call(args)
@@ -187,16 +216,31 @@ func (s *Store) notify() {
// and notify listeners of the change
func (s *Store) Set(data interface{}) error {
inType := reflect.TypeOf(data)
if data == nil {
return fmt.Errorf("cannot set store to nil")
}
if inType != s.dataType {
return fmt.Errorf("invalid data given in Store.Set(). Expected %s, got %s", s.dataType.String(), inType.String())
s.lock()
dataCopy := deepcopy.Copy(data)
if dataCopy != nil {
inType := reflect.TypeOf(dataCopy)
if inType != s.dataType && s.data.IsValid() {
s.unlock()
return fmt.Errorf("invalid data given in Store.Set(). Expected %s, got %s", s.dataType.String(), inType.String())
}
}
if s.dataType == nil {
s.dataType = reflect.TypeOf(dataCopy)
}
// Save data
s.mux.Lock()
s.data = reflect.ValueOf(data)
s.mux.Unlock()
s.data = reflect.ValueOf(dataCopy)
s.unlock()
// Resync with subscribers
s.resync()
@@ -247,7 +291,9 @@ func (s *Store) Subscribe(callback interface{}) {
callbackFunc := reflect.ValueOf(callback)
s.lock()
s.callbacks = append(s.callbacks, callbackFunc)
s.unlock()
}
// updaterCheck ensures the given function to Update() is
@@ -297,7 +343,9 @@ func (s *Store) Update(updater interface{}) {
}
// Build args
s.lock()
args := []reflect.Value{s.data}
s.unlock()
// Make call
results := reflect.ValueOf(updater).Call(args)
@@ -308,5 +356,12 @@ func (s *Store) Update(updater interface{}) {
// Get returns the value of the data that's kept in the current state / Store
func (s *Store) Get() interface{} {
s.lock()
defer s.unlock()
if !s.data.IsValid() {
return nil
}
return s.data.Interface()
}

View File

@@ -0,0 +1,165 @@
package runtime
import (
"context"
"math/rand"
"sync"
"testing"
"time"
internallogger "github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/logger"
is2 "github.com/matryer/is"
)
func TestStoreProvider_NewWithNilDefault(t *testing.T) {
is := is2.New(t)
defaultLogger := logger.NewDefaultLogger()
testLogger := internallogger.New(defaultLogger)
//testLogger.SetLogLevel(logger.TRACE)
serviceBus := servicebus.New(testLogger)
err := serviceBus.Start()
is.NoErr(err)
defer serviceBus.Stop()
testRuntime := New(serviceBus)
storeProvider := newStore(testRuntime)
testStore := storeProvider.New("test", 0)
// You should be able to write a new value into a
// store initialised with nil
err = testStore.Set(100)
is.NoErr(err)
// You shouldn't be able to write different types to the
// store
err = testStore.Set(false)
is.True(err != nil)
}
func TestStoreProvider_NewWithScalarDefault(t *testing.T) {
is := is2.New(t)
defaultLogger := logger.NewDefaultLogger()
testLogger := internallogger.New(defaultLogger)
//testLogger.SetLogLevel(logger.TRACE)
serviceBus := servicebus.New(testLogger)
err := serviceBus.Start()
is.NoErr(err)
defer serviceBus.Stop()
testRuntime := New(serviceBus)
storeProvider := newStore(testRuntime)
testStore := storeProvider.New("test", 100)
value := testStore.Get()
is.Equal(value, 100)
testStore.resync()
value = testStore.Get()
is.Equal(value, 100)
}
func TestStoreProvider_NewWithStructDefault(t *testing.T) {
is := is2.New(t)
defaultLogger := logger.NewDefaultLogger()
testLogger := internallogger.New(defaultLogger)
//testLogger.SetLogLevel(logger.TRACE)
serviceBus := servicebus.New(testLogger)
err := serviceBus.Start()
is.NoErr(err)
defer serviceBus.Stop()
testRuntime := New(serviceBus)
storeProvider := newStore(testRuntime)
type TestValue struct {
Name string
}
testValue := &TestValue{
Name: "hi",
}
testStore := storeProvider.New("test", testValue)
err = testStore.Set(testValue)
is.NoErr(err)
testStore.resync()
value := testStore.Get()
is.Equal(value, testValue)
is.Equal(value.(*TestValue).Name, "hi")
testValue = &TestValue{
Name: "there",
}
err = testStore.Set(testValue)
is.NoErr(err)
testStore.resync()
value = testStore.Get()
is.Equal(value, testValue)
is.Equal(value.(*TestValue).Name, "there")
}
func TestStoreProvider_RapidReadWrite(t *testing.T) {
is := is2.New(t)
defaultLogger := logger.NewDefaultLogger()
testLogger := internallogger.New(defaultLogger)
//testLogger.SetLogLevel(logger.TRACE)
serviceBus := servicebus.New(testLogger)
err := serviceBus.Start()
is.NoErr(err)
defer serviceBus.Stop()
testRuntime := New(serviceBus)
storeProvider := newStore(testRuntime)
testStore := storeProvider.New("test", 1)
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
var wg sync.WaitGroup
readers := 100
writers := 100
wg.Add(readers + writers)
// Setup readers
go func(testStore *Store, ctx context.Context) {
for readerCount := 0; readerCount < readers; readerCount++ {
go func(store *Store, ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
wg.Done()
return
default:
store.Get()
}
}
}(testStore, ctx, readerCount)
}
}(testStore, ctx)
// Setup writers
go func(testStore *Store, ctx context.Context) {
for writerCount := 0; writerCount < writers; writerCount++ {
go func(store *Store, ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
wg.Done()
return
default:
err := store.Set(rand.Int())
is.NoErr(err)
}
}
}(testStore, ctx, writerCount)
}
}(testStore, ctx)
wg.Wait()
}

View File

@@ -18,6 +18,8 @@ type Window interface {
Unminimise()
SetTitle(title string)
SetSize(width int, height int)
SetMinSize(width int, height int)
SetMaxSize(width int, height int)
SetPosition(x int, y int)
Fullscreen()
UnFullscreen()
@@ -85,6 +87,18 @@ func (w *window) SetSize(width int, height int) {
w.bus.Publish(message, "")
}
// SetSize sets the size of the window
func (w *window) SetMinSize(width int, height int) {
message := fmt.Sprintf("window:minsize:%d:%d", width, height)
w.bus.Publish(message, "")
}
// SetSize sets the size of the window
func (w *window) SetMaxSize(width int, height int) {
message := fmt.Sprintf("window:maxsize:%d:%d", width, height)
w.bus.Publish(message, "")
}
// SetPosition sets the position of the window
func (w *window) SetPosition(x int, y int) {
message := fmt.Sprintf("window:position:%d:%d", x, y)

View File

@@ -71,10 +71,11 @@ func (s *ServiceBus) Start() error {
}
// We run in a different thread
s.wg.Add(1)
go func() {
quit := false
s.wg.Add(1)
// Loop until we get a quit message
for !quit {

View File

@@ -83,7 +83,7 @@ func (r *Runtime) Start() error {
if r.startupCallback != nil {
go r.startupCallback(r.runtime)
} else {
r.logger.Error("no startup callback registered!")
r.logger.Warning("no startup callback registered!")
}
default:
r.logger.Error("unknown hook message: %+v", hooksMessage)
@@ -131,6 +131,8 @@ func (r *Runtime) GoRuntime() *runtime.Runtime {
func (r *Runtime) shutdown() {
if r.shutdownCallback != nil {
go r.shutdownCallback()
} else {
r.logger.Warning("no shutdown callback registered!")
}
r.logger.Trace("Shutdown")
}

View File

@@ -2,14 +2,21 @@ package main
import (
"github.com/wailsapp/wails/v2"
"log"
)
func main() {
// Create application with options
app := wails.CreateApp("{{.ProjectName}}", 1024, 768)
app, err := wails.CreateApp("{{.ProjectName}}", 1024, 768)
if err != nil {
log.Fatal(err)
}
app.Bind(newBasic())
app.Run()
err = app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@@ -20,6 +20,14 @@ type WebClient struct {
running bool
}
func (wc *WebClient) SetTrayMenu(trayMenuJSON string) {
wc.logger.Info("Not implemented in server build")
}
func (wc *WebClient) UpdateTrayMenuLabel(trayMenuJSON string) {
wc.logger.Info("Not implemented in server build")
}
func (wc *WebClient) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) {
wc.logger.Info("Not implemented in server build")
}

View File

@@ -211,7 +211,6 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
options.CompiledBinary = compiledBinary
// Create the command
fmt.Printf("Compile command: %+v", commands.AsSlice())
cmd := exec.Command(options.Compiler, commands.AsSlice()...)
// Set the directory

View File

@@ -104,7 +104,7 @@ func Build(options *Options) (string, error) {
// return "", err
// }
if !options.IgnoreFrontend {
outputLogger.Println(" - Building Wails Frontend")
outputLogger.Println(" - Building Project Frontend")
err = builder.BuildFrontend(outputLogger)
if err != nil {
return "", err

View File

@@ -28,11 +28,18 @@ type MenuItem struct {
// Callback function when menu clicked
Click Callback `json:"-"`
// Foreground colour in hex RGBA format EG: 0xFF0000FF = #FF0000FF = red
Foreground int
// Colour
RGBA string
// Background colour
Background int
// Font
FontSize int
FontName string
// Image - base64 image data
Image string
// Tooltip
Tooltip string
// This holds the menu item's parent.
parent *MenuItem

View File

@@ -1,11 +1,12 @@
package options
import (
wailsruntime "github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/pkg/menu"
"log"
"runtime"
wailsruntime "github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/imdario/mergo"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options/mac"
@@ -33,6 +34,7 @@ type App struct {
LogLevel logger.LogLevel
Startup func(*wailsruntime.Runtime) `json:"-"`
Shutdown func() `json:"-"`
Bind []interface{}
}
// MergeDefaults will set the minimum default values for an application

View File

@@ -58,6 +58,7 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

View File

@@ -14,19 +14,12 @@ type Runtime = runtime.Runtime
// Store is an alias for the Store object
type Store = runtime.Store
// CreateAppWithOptions creates an application based on the given config
func CreateAppWithOptions(options *options.App) (*app.App, error) {
return app.CreateApp(options)
}
// CreateApp creates an application based on the given title, width and height
func CreateApp(title string, width int, height int) (*app.App, error) {
options := &options.App{
Title: title,
Width: width,
Height: height,
// Run creates an application based on the given config and executes it
func Run(options *options.App) error {
app, err := app.CreateApp(options)
if err != nil {
return err
}
return app.CreateApp(options)
return app.Run()
}