5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-03 02:50:36 +08:00

[windows-x] Support events runtime js->go, Refactor events methods, Refactor JS runtime.

This commit is contained in:
Lea Anthony 2021-08-01 22:14:33 +10:00
parent 244b3dc2b4
commit 619d8cc05e
16 changed files with 574 additions and 96 deletions

View File

@ -5,6 +5,7 @@ import (
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/dispatcher"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/internal/signal"
@ -26,88 +27,18 @@ type App struct {
// Startup/Shutdown
startupCallback func(ctx context.Context)
shutdownCallback func()
ctx context.Context
}
func (a *App) Run() error {
go func() {
//time.Sleep(3 * time.Second)
//println("WindowSetSize(3000,2000)")
//a.frontend.WindowSetSize(3000,2000)
//x,y := a.frontend.WindowGetSize()
//println("X", x, "Y", y)
//time.Sleep(3 * time.Second)
//println("a.frontend.WindowSetSize(10,10)")
//a.frontend.WindowSetSize(10,10)
//x,y = a.frontend.WindowGetSize()
//println("X", x, "Y", y)
//time.Sleep(3 * time.Second)
//time.Sleep(3 * time.Second)
//println("WindowSetMaxSize(50,50)")
//a.frontend.WindowSetMaxSize(200,200)
//x,y := a.frontend.WindowGetSize()
//println("X", x, "Y", y)
//time.Sleep(3 * time.Second)
//println("WindowSetMinSize(100,100)")
//a.frontend.WindowSetMinSize(600,600)
//x,y = a.frontend.WindowGetSize()
//println("X", x, "Y", y)
//println("fullscreen")
//a.frontend.WindowFullscreen()
//time.Sleep(1 * time.Second)
//println("unfullscreen")
//a.frontend.WindowUnFullscreen()
//time.Sleep(1 * time.Second)
//println("hide")
//a.frontend.WindowHide()
//time.Sleep(1 * time.Second)
//println("show")
//a.frontend.WindowShow()
//time.Sleep(1 * time.Second)
//println("title 1")
//a.frontend.WindowSetTitle("title 1")
//time.Sleep(1 * time.Second)
//println("title 2")
//a.frontend.WindowSetTitle("title 2")
//time.Sleep(1 * time.Second)
//println("setsize 1")
//a.frontend.WindowSetSize(100,100)
//time.Sleep(1 * time.Second)
//println("setsize 2")
//a.frontend.WindowSetSize(500,500)
//time.Sleep(1 * time.Second)
//println("setpos 1")
//a.frontend.WindowSetPos(0,0)
//time.Sleep(1 * time.Second)
//println("setpos 2")
//a.frontend.WindowSetPos(500,500)
//time.Sleep(1 * time.Second)
//println("Center 1")
//a.frontend.WindowCenter()
//time.Sleep(5 * time.Second)
//println("Center 2")
//a.frontend.WindowCenter()
//time.Sleep(1 * time.Second)
//println("maximise")
//a.frontend.WindowMaximise()
//time.Sleep(1 * time.Second)
//println("UnMaximise")
//a.frontend.WindowUnmaximise()
//time.Sleep(1 * time.Second)
//println("minimise")
//a.frontend.WindowMinimise()
//time.Sleep(1 * time.Second)
//println("unminimise")
//a.frontend.WindowUnminimise()
//time.Sleep(1 * time.Second)
}()
return a.frontend.Run()
return a.frontend.Run(a.ctx)
}
// CreateApp
func CreateApp(appoptions *options.App) (*App, error) {
ctx := context.Background()
// Merge default options
options.MergeDefaults(appoptions)
@ -128,12 +59,14 @@ func CreateApp(appoptions *options.App) (*App, error) {
// Create binding exemptions - Ugly hack. There must be a better way
bindingExemptions := []interface{}{appoptions.Startup, appoptions.Shutdown}
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions)
messageDispatcher := dispatcher.NewDispatcher(myLogger, appBindings)
eventHandler := runtime.NewEvents(myLogger)
ctx = context.WithValue(ctx, "events", eventHandler)
messageDispatcher := dispatcher.NewDispatcher(myLogger, appBindings, eventHandler)
appFrontend := NewFrontend(appoptions, myLogger, appBindings, messageDispatcher)
result := &App{
ctx: ctx,
frontend: appFrontend,
logger: myLogger,
menuManager: menuManager,

View File

@ -0,0 +1,36 @@
package dispatcher
import (
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/logger"
)
type Dispatcher struct {
log *logger.Logger
bindings *binding.Bindings
events frontend.Events
}
func NewDispatcher(log *logger.Logger, bindings *binding.Bindings, events frontend.Events) *Dispatcher {
return &Dispatcher{
log: log,
bindings: bindings,
events: events,
}
}
func (d *Dispatcher) ProcessMessage(message string) error {
if message == "" {
return errors.New("No message to process")
}
switch message[0] {
case 'L':
return d.processLogMessage(message)
case 'E':
return d.processEventMessage(message)
default:
return errors.New("Unknown message from front end: " + message)
}
}

View File

@ -0,0 +1,32 @@
package dispatcher
import (
"encoding/json"
"errors"
)
type EventMessage struct {
Name string `json:"name"`
Data []interface{} `json:"data"`
}
func (d *Dispatcher) processEventMessage(message string) error {
if len(message) < 3 {
return errors.New("Invalid Event Message: " + message)
}
switch message[1] {
case 'E':
var eventMessage EventMessage
err := json.Unmarshal([]byte(message[2:]), &eventMessage)
if err != nil {
return err
}
go d.events.Notify(eventMessage.Name, eventMessage.Data)
case 'X':
eventName := message[2:]
go d.events.Off(eventName)
}
return nil
}

View File

@ -0,0 +1,49 @@
package dispatcher
import (
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/internal/logger"
pkgLogger "github.com/wailsapp/wails/v2/pkg/logger"
)
var logLevelMap = map[byte]logger.LogLevel{
'1': pkgLogger.TRACE,
'2': pkgLogger.DEBUG,
'3': pkgLogger.INFO,
'4': pkgLogger.WARNING,
'5': pkgLogger.ERROR,
}
func (d *Dispatcher) processLogMessage(message string) error {
if len(message) < 3 {
return errors.New("Invalid Log Message: " + message)
}
messageText := message[2:]
switch message[1] {
case 'T':
d.log.Trace(messageText)
case 'P':
d.log.Print(messageText)
case 'D':
d.log.Debug(messageText)
case 'I':
d.log.Info(messageText)
case 'W':
d.log.Warning(messageText)
case 'E':
d.log.Error(messageText)
case 'F':
d.log.Fatal(messageText)
case 'S':
loglevel, exists := logLevelMap[message[2]]
if !exists {
return errors.New("Invalid Set Log Level Message: " + message)
}
d.log.SetLogLevel(loglevel)
default:
return errors.New("Invalid Log Message: " + message)
}
return nil
}

View File

@ -0,0 +1,10 @@
package frontend
type Events interface {
On(eventName string, callback func(...interface{}))
OnMultiple(eventName string, callback func(...interface{}), counter int)
Once(eventName string, callback func(...interface{}))
Emit(eventName string, data ...interface{})
Off(eventName string)
Notify(name string, data ...interface{})
}

View File

@ -1,6 +1,9 @@
package frontend
import "github.com/wailsapp/wails/v2/pkg/menu"
import (
"context"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// FileFilter defines a filter for dialog boxes
type FileFilter struct {
@ -54,15 +57,9 @@ type MessageDialogOptions struct {
}
type Frontend interface {
// Main methods
Run() error
Run(context.Context) error
Quit()
//// Events
//NotifyEvent(message string)
//CallResult(message string)
//
// Dialog
OpenFileDialog(dialogOptions OpenDialogOptions) (string, error)
OpenMultipleFilesDialog(dialogOptions OpenDialogOptions) ([]string, error)

View File

@ -0,0 +1,160 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import {SendMessage} from './ipc';
// Defines a single listener with a maximum number of times to callback
/**
* The Listener class defines a listener! :-)
*
* @class Listener
*/
class Listener {
/**
* Creates an instance of Listener.
* @param {function} callback
* @param {number} maxCallbacks
* @memberof Listener
*/
constructor(callback, maxCallbacks) {
// Default of -1 means infinite
maxCallbacks = maxCallbacks || -1;
// Callback invokes the callback with the given data
// Returns true if this listener should be destroyed
this.Callback = (data) => {
callback.apply(null, data);
// If maxCallbacks is infinite, return false (do not destroy)
if (maxCallbacks === -1) {
return false;
}
// Decrement maxCallbacks. Return true if now 0, otherwise false
maxCallbacks -= 1;
return maxCallbacks === 0;
};
}
}
let eventListeners = {};
/**
* Registers an event listener that will be invoked `maxCallbacks` times before being destroyed
*
* @export
* @param {string} eventName
* @param {function} callback
* @param {number} maxCallbacks
*/
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
eventListeners[eventName] = eventListeners[eventName] || [];
const thisListener = new Listener(callback, maxCallbacks);
eventListeners[eventName].push(thisListener);
}
/**
* Registers an event listener that will be invoked every time the event is emitted
*
* @export
* @param {string} eventName
* @param {function} callback
*/
export function EventsOn(eventName, callback) {
EventsOnMultiple(eventName, callback, -1);
}
/**
* Registers an event listener that will be invoked once then destroyed
*
* @export
* @param {string} eventName
* @param {function} callback
*/
export function EventsOnce(eventName, callback) {
EventsOnMultiple(eventName, callback, 1);
}
function notifyListeners(eventData) {
// Get the event name
let eventName = eventData.name;
// Check if we have any listeners for this event
if (eventListeners[eventName]) {
// Keep a list of listener indexes to destroy
const newEventListenerList = eventListeners[eventName].slice();
// Iterate listeners
for (let count = 0; count < eventListeners[eventName].length; count += 1) {
// Get next listener
const listener = eventListeners[eventName][count];
let data = eventData.data;
// Do the callback
const destroy = listener.Callback(data);
if (destroy) {
// if the listener indicated to destroy itself, add it to the destroy list
newEventListenerList.splice(count, 1);
}
}
// Update callbacks with new list of listeners
eventListeners[eventName] = newEventListenerList;
}
}
/**
* Notify informs frontend listeners that an event was emitted with the given data
*
* @export
* @param {string} notifyMessage - encoded notification message
*/
export function EventsNotify(notifyMessage) {
// Parse the message
let message;
try {
message = JSON.parse(notifyMessage);
} catch (e) {
const error = 'Invalid JSON passed to Notify: ' + notifyMessage;
throw new Error(error);
}
notifyListeners(message);
}
/**
* Emit an event with the given name and data
*
* @export
* @param {string} eventName
*/
export function EventsEmit(eventName) {
const payload = {
name: eventName,
data: [].slice.apply(arguments).slice(1),
};
// Notify JS listeners
notifyListeners(payload);
// Notify Go listeners
SendMessage('EE' + JSON.stringify(payload));
}
export function EventsOff(eventName) {
// Notify Go listeners
SendMessage('EX' + eventName);
}

View File

@ -31,7 +31,7 @@ function sendLogMessage(level, message) {
* @export
* @param {string} message
*/
export function Trace(message) {
export function LogTrace(message) {
sendLogMessage('T', message);
}
@ -41,7 +41,7 @@ export function Trace(message) {
* @export
* @param {string} message
*/
export function Print(message) {
export function LogPrint(message) {
sendLogMessage('P', message);
}
@ -51,7 +51,7 @@ export function Print(message) {
* @export
* @param {string} message
*/
export function Debug(message) {
export function LogDebug(message) {
sendLogMessage('D', message);
}
@ -61,7 +61,7 @@ export function Debug(message) {
* @export
* @param {string} message
*/
export function Info(message) {
export function LogInfo(message) {
sendLogMessage('I', message);
}
@ -71,7 +71,7 @@ export function Info(message) {
* @export
* @param {string} message
*/
export function Warning(message) {
export function LogWarning(message) {
sendLogMessage('W', message);
}
@ -81,7 +81,7 @@ export function Warning(message) {
* @export
* @param {string} message
*/
export function Error(message) {
export function LogError(message) {
sendLogMessage('E', message);
}
@ -91,7 +91,7 @@ export function Error(message) {
* @export
* @param {string} message
*/
export function Fatal(message) {
export function LogFatal(message) {
sendLogMessage('F', message);
}
@ -106,7 +106,7 @@ export function SetLogLevel(loglevel) {
}
// Log levels
export const Level = {
export const LogLevel = {
TRACE: 1,
DEBUG: 2,
INFO: 3,

View File

@ -0,0 +1,43 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 9 */
import * as Log from './log';
import {EventsEmit, EventsNotify, EventsOff, EventsOn, EventsOnce, EventsOnMultiple} from './events';
// import {Callback, SystemCall} from './calls';
// import {AddScript, DisableDefaultContextMenu, InjectCSS} from './utils';
// import {AddIPCListener, SendMessage} from 'ipc';
// Backend is where the Go struct wrappers get bound to
window.backend = {};
window.runtime = {
...Log,
EventsOn,
EventsOnce,
EventsOnMultiple,
EventsEmit,
EventsOff,
};
// Initialise global if not already
window.wails = {
_: {
// Callback,
EventsNotify,
// AddScript,
// InjectCSS,
// DisableDefaultContextMenu,
// // Init,
// AddIPCListener,
// SystemCall,
// SendMessage,
},
};

View File

@ -0,0 +1,135 @@
package runtime
import (
"github.com/wailsapp/wails/v2/internal/logger"
"sync"
)
// eventListener holds a callback function which is invoked when
// the event listened for is emitted. It has a counter which indicates
// how the total number of events it is interested in. A value of zero
// means it does not expire (default).
type eventListener struct {
callback func(...interface{}) // Function to call with emitted event data
counter int // The number of times this callback may be called. -1 = infinite
delete bool // Flag to indicate that this listener should be deleted
}
// Events handles eventing
type Events struct {
log *logger.Logger
// Go event listeners
listeners map[string][]*eventListener
notifyLock sync.RWMutex
}
func (e *Events) Notify(name string, data ...interface{}) {
e.notify(name, data...)
}
func (e *Events) On(eventName string, callback func(...interface{})) {
e.registerListener(eventName, callback, -1)
}
func (e *Events) OnMultiple(eventName string, callback func(...interface{}), counter int) {
e.registerListener(eventName, callback, counter)
}
func (e *Events) Once(eventName string, callback func(...interface{})) {
e.registerListener(eventName, callback, 1)
}
func (e *Events) Emit(eventName string, data ...interface{}) {
e.notify(eventName, data...)
}
func (e *Events) Off(eventName string) {
e.unRegisterListener(eventName)
}
// NewEvents creates a new log subsystem
func NewEvents(log *logger.Logger) *Events {
result := &Events{
log: log,
listeners: make(map[string][]*eventListener),
}
return result
}
// registerListener provides a means of subscribing to events of type "eventName"
func (e *Events) registerListener(eventName string, callback func(...interface{}), counter int) {
// Create new eventListener
thisListener := &eventListener{
callback: callback,
counter: counter,
delete: false,
}
e.notifyLock.Lock()
// Append the new listener to the listeners slice
e.listeners[eventName] = append(e.listeners[eventName], thisListener)
e.notifyLock.Unlock()
}
// unRegisterListener provides a means of unsubscribing to events of type "eventName"
func (e *Events) unRegisterListener(eventName string) {
e.notifyLock.Lock()
// Clear the listeners
delete(e.listeners, eventName)
e.notifyLock.Unlock()
}
// notify for the given event name
func (e *Events) notify(eventName string, data ...interface{}) {
// Get list of event listeners
listeners := e.listeners[eventName]
if listeners == nil {
e.log.Trace("No listeners for event '%s'", eventName)
return
}
// Lock the listeners
e.notifyLock.Lock()
// We have a dirty flag to indicate that there are items to delete
itemsToDelete := false
// Callback in goroutine
for _, listener := range listeners {
if listener.counter > 0 {
listener.counter--
}
go listener.callback(data...)
if listener.counter == 0 {
listener.delete = true
itemsToDelete = true
}
}
// Do we have items to delete?
if itemsToDelete == true {
// Create a new Listeners slice
var newListeners []*eventListener
// Iterate over current listeners
for _, listener := range listeners {
// If we aren't deleting the listener, add it to the new list
if !listener.delete {
newListeners = append(newListeners, listener)
}
}
// Save new listeners or remove entry
if len(newListeners) > 0 {
e.listeners[eventName] = newListeners
} else {
delete(e.listeners, eventName)
}
}
// Unlock
e.notifyLock.Unlock()
}

View File

@ -0,0 +1,14 @@
{
"name": "runtime",
"version": "2.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"esbuild": {
"version": "0.12.17",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.17.tgz",
"integrity": "sha512-GshKJyVYUnlSXIZj/NheC2O0Kblh42CS7P1wJyTbbIHevTG4jYMS9NNw8EOd8dDWD0dzydYHS01MpZoUcQXB4g==",
"dev": true
}
}
}

View File

@ -0,0 +1,15 @@
{
"name": "runtime",
"version": "2.0.0",
"description": "Wails JS Runtime",
"main": "index.js",
"scripts": {
"build:windows": "esbuild desktop/main.js --bundle --minify --outfile=runtime_windows.js --define:PLATFORM='windows'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "ISC",
"devDependencies": {
"esbuild": "^0.12.17"
}
}

View File

@ -0,0 +1 @@
(()=>{var l=Object.defineProperty;var x=t=>l(t,"__esModule",{value:!0});var h=(t,e)=>{x(t);for(var n in e)l(t,n,{get:e[n],enumerable:!0})};var p={};h(p,{LogDebug:()=>M,LogError:()=>R,LogFatal:()=>I,LogInfo:()=>N,LogLevel:()=>A,LogPrint:()=>y,LogTrace:()=>m,LogWarning:()=>S,SetLogLevel:()=>P});var f=[];function s(t){if(window.chrome.webview.postMessage(t),f.length>0)for(let e=0;e<f.length;e++)f[e](t)}function i(t,e){s("L"+t+e)}function m(t){i("T",t)}function y(t){i("P",t)}function M(t){i("D",t)}function N(t){i("I",t)}function S(t){i("W",t)}function R(t){i("E",t)}function I(t){i("F",t)}function P(t){i("S",t)}var A={TRACE:1,DEBUG:2,INFO:3,WARNING:4,ERROR:5};var u=class{constructor(e,n){n=n||-1,this.Callback=o=>(e.apply(null,o),n===-1?!1:(n-=1,n===0))}},r={};function c(t,e,n){r[t]=r[t]||[];let o=new u(e,n);r[t].push(o)}function E(t,e){c(t,e,-1)}function d(t,e){c(t,e,1)}function g(t){let e=t.name;if(r[e]){let n=r[e].slice();for(let o=0;o<r[e].length;o+=1){let v=r[e][o],O=t.data;v.Callback(O)&&n.splice(o,1)}r[e]=n}}function a(t){let e;try{e=JSON.parse(t)}catch(n){let o="Invalid JSON passed to Notify: "+t;throw new Error(o)}g(e)}function L(t){let e={name:t,data:[].slice.apply(arguments).slice(1)};g(e),s("EE"+JSON.stringify(e))}function w(t){s("EX"+t)}window.backend={};window.runtime={...p,EventsOn:E,EventsOnce:d,EventsOnMultiple:c,EventsEmit:L,EventsOff:w};window.wails={_:{EventsNotify:a}};})();

View File

@ -16,6 +16,10 @@ import (
)
type Frontend struct {
// Context
ctx context.Context
frontendOptions *options.App
logger *logger.Logger
chromium *edge.Chromium
@ -30,7 +34,7 @@ type Frontend struct {
dispatcher frontend.Dispatcher
}
func (f *Frontend) Run() error {
func (f *Frontend) Run(ctx context.Context) error {
mainWindow := NewWindow(nil, f.frontendOptions)
f.mainWindow = mainWindow
@ -57,7 +61,7 @@ func (f *Frontend) Run() error {
// TODO: Move this into a callback from frontend
go func() {
ctx := context.WithValue(context.Background(), "frontend", f)
ctx := context.WithValue(ctx, "frontend", f)
f.frontendOptions.Startup(ctx)
}()

View File

@ -0,0 +1,38 @@
// +build experimental
package runtime
import (
"context"
)
// EventsOn registers a listener for the given event name
func EventsOn(ctx context.Context, eventName string, callback func(optionalData ...interface{})) {
events := getEvents(ctx)
events.On(eventName, callback)
}
// EventsOff unregisters a listener for the given event name
func EventsOff(ctx context.Context, eventName string) {
events := getEvents(ctx)
events.Off(eventName)
}
// EventsOnce registers a listener for the given event name. After the first callback, the
// listener is deleted.
func EventsOnce(ctx context.Context, eventName string, callback func(optionalData ...interface{})) {
events := getEvents(ctx)
events.Once(eventName, callback)
}
// EventsOnMultiple registers a listener for the given event name, that may be called a maximum of 'counter' times
func EventsOnMultiple(ctx context.Context, eventName string, callback func(optionalData ...interface{}), counter int) {
events := getEvents(ctx)
events.OnMultiple(eventName, callback, counter)
}
// EventsEmit pass through
func EventsEmit(ctx context.Context, eventName string, optionalData ...interface{}) {
events := getEvents(ctx)
events.Emit(eventName, optionalData...)
}

View File

@ -20,8 +20,19 @@ func getFrontend(ctx context.Context) frontend.Frontend {
return nil
}
func getEvents(ctx context.Context) frontend.Events {
result := ctx.Value("events")
if result != nil {
return result.(frontend.Events)
}
pc, _, _, _ := goruntime.Caller(1)
funcName := goruntime.FuncForPC(pc).Name()
log.Fatalf("cannot call '%s': Application not initialised", funcName)
return nil
}
// Quit the application
func Quit(ctx context.Context) {
frontend := getFrontend(ctx)
frontend.Quit()
appFrontend := getFrontend(ctx)
appFrontend.Quit()
}