diff --git a/v2/internal/appng/app.go b/v2/internal/appng/app.go index d1c1f0fbe..49fb1aeff 100644 --- a/v2/internal/appng/app.go +++ b/v2/internal/appng/app.go @@ -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, diff --git a/v2/internal/frontend/dispatcher/dispatcher.go b/v2/internal/frontend/dispatcher/dispatcher.go new file mode 100644 index 000000000..3ca9fbaa1 --- /dev/null +++ b/v2/internal/frontend/dispatcher/dispatcher.go @@ -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) + } +} diff --git a/v2/internal/frontend/dispatcher/events.go b/v2/internal/frontend/dispatcher/events.go new file mode 100644 index 000000000..7a02eb09d --- /dev/null +++ b/v2/internal/frontend/dispatcher/events.go @@ -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 +} diff --git a/v2/internal/frontend/dispatcher/log.go b/v2/internal/frontend/dispatcher/log.go new file mode 100644 index 000000000..ead44eeb1 --- /dev/null +++ b/v2/internal/frontend/dispatcher/log.go @@ -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 +} diff --git a/v2/internal/frontend/events.go b/v2/internal/frontend/events.go new file mode 100644 index 000000000..3ba822e67 --- /dev/null +++ b/v2/internal/frontend/events.go @@ -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{}) +} diff --git a/v2/internal/frontend/frontend.go b/v2/internal/frontend/frontend.go index 3c2bb0b62..e18ee3471 100644 --- a/v2/internal/frontend/frontend.go +++ b/v2/internal/frontend/frontend.go @@ -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) diff --git a/v2/internal/frontend/runtime/desktop/events.js b/v2/internal/frontend/runtime/desktop/events.js new file mode 100644 index 000000000..403f7541a --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/events.js @@ -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); +} \ No newline at end of file diff --git a/v2/internal/frontend/runtime/desktop/log.js b/v2/internal/frontend/runtime/desktop/log.js index 160cdc534..ed045f12c 100644 --- a/v2/internal/frontend/runtime/desktop/log.js +++ b/v2/internal/frontend/runtime/desktop/log.js @@ -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, diff --git a/v2/internal/frontend/runtime/desktop/main.js b/v2/internal/frontend/runtime/desktop/main.js new file mode 100644 index 000000000..ff2eabd3b --- /dev/null +++ b/v2/internal/frontend/runtime/desktop/main.js @@ -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, + }, +}; + diff --git a/v2/internal/frontend/runtime/events.go b/v2/internal/frontend/runtime/events.go new file mode 100644 index 000000000..b6032bf85 --- /dev/null +++ b/v2/internal/frontend/runtime/events.go @@ -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() +} diff --git a/v2/internal/frontend/runtime/package-lock.json b/v2/internal/frontend/runtime/package-lock.json new file mode 100644 index 000000000..d72319acc --- /dev/null +++ b/v2/internal/frontend/runtime/package-lock.json @@ -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 + } + } +} diff --git a/v2/internal/frontend/runtime/package.json b/v2/internal/frontend/runtime/package.json new file mode 100644 index 000000000..b2f9947ec --- /dev/null +++ b/v2/internal/frontend/runtime/package.json @@ -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 ", + "license": "ISC", + "devDependencies": { + "esbuild": "^0.12.17" + } +} diff --git a/v2/internal/frontend/runtime/runtime_windows.js b/v2/internal/frontend/runtime/runtime_windows.js new file mode 100644 index 000000000..c31b8e88e --- /dev/null +++ b/v2/internal/frontend/runtime/runtime_windows.js @@ -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(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