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

Event->CustomEvent to prevent potential clash with native JS Event object

Support Eventing
This commit is contained in:
Lea Anthony 2023-02-04 11:27:27 +11:00
parent 0e8144e52f
commit f26756be9c
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
22 changed files with 1139 additions and 80 deletions

View File

@ -20,15 +20,15 @@ Informal and incomplete list of things needed in v3.
- [ ] Implement runtime dispatcher
- [ ] Log
- [x] Same Window
- [ ] Other Window
- [ ] Dialogs
- [x] Other Window
- [x] Dialogs
- [x] Info
- [x] Warning
- [x] Error
- [x] Question
- [x] OpenFile
- [x] SaveFile
- [ ] Events
- [x] Events
- [ ] Screens
- [x] Clipboard
- [ ] Application

53
v3/V3 Changes.md Normal file
View File

@ -0,0 +1,53 @@
# Changes for v3
## Events
In v3, there are 3 types of events:
- Application Events
- Window Events
- Custom Events
### Application Events
Application events are events that are emitted by the application. These events include native events such as `ApplicationDidFinishLaunching` on macOS.
### Window Events
Window events are events that are emitted by a window. These events include native events such as `WindowDidBecomeMain` on macOS.
### Custom Events
Events that the user defines are called `CustomEvents`. This is to differentiate them from the `Event` object that is used to communicate with the browser. CustomEvents are now objects that encapsulate all the details of an event. This includes the event name, the data, and the source of the event.
The data associated with a CustomEvent is now a single value. If multiple values are required, then a struct can be used.
### Event callbacks and `Emit` function signature
The signatures events callbacks (as used by `On`, `Once` & `OnMultiple`) have changed. In v2, the callback function received optional data. In v3, the callback function receives a `CustomEvent` object that contains all data related to the event.
Similarly, the `Emit` function has changed. Instead of taking a name and optional data, it now takes a single `CustomEvent` object that it will emit.
### `Off` and `OffAll`
In v2, `Off` and `OffAll` calls would remove events in both JS and Go. Due to the multi-window nature of v3, this has been changed so that these methods only apply to the context they are called in. For example, if you call `Off` in a window, it will only remove events for that window. If you use `Off` in Go, it will only remove events for Go.
### Developer notes
When emitting an event in Go, it will dispatch the event to local Go listeners and also each window in the application.
When emitting an event in JS, it now sends the event to the application. This will be processed as if it was emitted in Go, however the sender ID will be that of the window.
## Window
TBD
## ClipBoard
TBD
## Bindings
TBD
## Dialogs

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>body{ text-align: center; color: white; background-color: rgba(0,0,0,0); user-select: none; -ms-user-select: none; -webkit-user-select: none; }</style>
</head>
<body>
<h1>Events Demo</h1>
<br/>
<div id="results"></div>
</body>
<script>
wails.Events.On("myevent", function(data) {
let currentHTML = document.getElementById("results").innerHTML;
document.getElementById("results").innerHTML = currentHTML + "<br/>" + JSON.stringify(data);;
})
</script>
</html>

View File

@ -0,0 +1,72 @@
package main
import (
"embed"
_ "embed"
"log"
"time"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/options"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed assets
var assets embed.FS
func main() {
app := application.New(options.Application{
Name: "Events Demo",
Description: "A demo of the Events API",
Mac: options.Mac{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
app.Events.On("myevent", func(e *application.CustomEvent) {
log.Printf("[Go] CustomEvent received: %+v\n", e)
})
app.On(events.Mac.ApplicationDidFinishLaunching, func() {
for {
log.Println("Sending event")
app.Events.Emit(&application.CustomEvent{
Name: "myevent",
Data: "hello",
})
time.Sleep(10 * time.Second)
}
})
app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Title: "Events Demo",
Assets: options.Assets{
FS: assets,
},
Mac: options.MacWindow{
Backdrop: options.MacBackdropTranslucent,
TitleBar: options.TitleBarHiddenInsetUnified,
InvisibleTitleBarHeight: 50,
},
})
app.NewWebviewWindowWithOptions(&options.WebviewWindow{
Title: "Events Demo",
Assets: options.Assets{
FS: assets,
},
Mac: options.MacWindow{
Backdrop: options.MacBackdropTranslucent,
TitleBar: options.TitleBarHiddenInsetUnified,
InvisibleTitleBarHeight: 50,
},
})
err := app.Run()
if err != nil {
log.Fatal(err.Error())
}
}

View File

@ -9,6 +9,7 @@ require (
github.com/leaanthony/clir v1.6.0
github.com/leaanthony/gosod v1.0.3
github.com/leaanthony/winicon v1.0.0
github.com/matryer/is v1.4.0
github.com/pterm/pterm v0.12.51
github.com/samber/lo v1.37.0
github.com/stretchr/testify v1.8.1
@ -57,4 +58,4 @@ require (
mvdan.cc/sh/v3 v3.6.0 // indirect
)
replace github.com/wailsapp/wails/v2 => ../v2
replace github.com/wailsapp/wails/v2 => ../v2

View File

@ -127,8 +127,6 @@ github.com/tc-hib/winres v0.1.6 h1:qgsYHze+BxQPEYilxIz/KCQGaClvI2+yLBAZs+3+0B8=
github.com/tc-hib/winres v0.1.6/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 h1:Wn+nhnS+VytzE0PegUzSh4T3hXJCtggKGD/4U5H9+wQ=
github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6/go.mod h1:zlNLI0E2c2qA6miiuAHtp0Bac8FaGH0tlhA19OssR/8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

View File

@ -0,0 +1,187 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 9 */
import {newRuntimeCaller} from "./runtime";
let call = newRuntimeCaller("events");
/**
* The Listener class defines a listener! :-)
*
* @class Listener
*/
class Listener {
/**
* Creates an instance of Listener.
* @param {string} eventName
* @param {function} callback
* @param {number} maxCallbacks
* @memberof Listener
*/
constructor(eventName, callback, maxCallbacks) {
this.eventName = eventName;
// Default of -1 means infinite
this.maxCallbacks = maxCallbacks || -1;
// Callback invokes the callback with the given data
// Returns true if this listener should be destroyed
this.Callback = (data) => {
callback(data);
// If maxCallbacks is infinite, return false (do not destroy)
if (this.maxCallbacks === -1) {
return false;
}
// Decrement maxCallbacks. Return true if now 0, otherwise false
this.maxCallbacks -= 1;
return this.maxCallbacks === 0;
};
}
}
/**
* CustomEvent defines a custom event. It is passed to event listeners.
*
* @class CustomEvent
*/
export class CustomEvent {
/**
* Creates an instance of CustomEvent.
* @param {string} name - Name of the event
* @param {any} data - Data associated with the event
* @memberof CustomEvent
*/
constructor(name, data) {
this.name = name;
this.data = data;
}
}
export const eventListeners = new Map();
/**
* Registers an event listener that will be invoked `maxCallbacks` times before being destroyed
*
* @export
* @param {string} eventName
* @param {function(CustomEvent): void} callback
* @param {number} maxCallbacks
* @returns {function} A function to cancel the listener
*/
export function OnMultiple(eventName, callback, maxCallbacks) {
let listeners = eventListeners.get(eventName) || [];
const thisListener = new Listener(eventName, callback, maxCallbacks);
listeners.push(thisListener);
eventListeners.set(eventName, listeners);
return () => listenerOff(thisListener);
}
/**
* Registers an event listener that will be invoked every time the event is emitted
*
* @export
* @param {string} eventName
* @param {function(CustomEvent): void} callback
* @returns {function} A function to cancel the listener
*/
export function On(eventName, callback) {
return OnMultiple(eventName, callback, -1);
}
/**
* Registers an event listener that will be invoked once then destroyed
*
* @export
* @param {string} eventName
* @param {function(CustomEvent): void} callback
* @returns {function} A function to cancel the listener
*/
export function Once(eventName, callback) {
return OnMultiple(eventName, callback, 1);
}
/**
* listenerOff unregisters a listener previously registered with On
*
* @param {Listener} listener
*/
function listenerOff(listener) {
const eventName = listener.eventName;
// Remove local listener
let listeners = eventListeners.get(eventName).filter(l => l !== listener);
if (listeners.length === 0) {
eventListeners.delete(eventName);
} else {
eventListeners.set(eventName, listeners);
}
}
/**
* dispatches an event to all listeners
*
* @export
* @param {CustomEvent} event
*/
export function dispatchCustomEvent(event) {
console.log("dispatching event: ", {event});
let listeners = eventListeners.get(event.name);
if (listeners) {
// iterate listeners and call callback. If callback returns true, remove listener
let toRemove = [];
listeners.forEach(listener => {
let remove = listener.Callback(event)
if (remove) {
toRemove.push(listener);
}
});
// remove listeners
if (toRemove.length > 0) {
listeners = listeners.filter(l => !toRemove.includes(l));
if (listeners.length === 0) {
eventListeners.delete(event.name);
} else {
eventListeners.set(event.name, listeners);
}
}
}
}
/**
* Off unregisters a listener previously registered with On,
* optionally multiple listeners can be unregistered via `additionalEventNames`
*
[v3 CHANGE] Off only unregisters listeners within the current window
*
* @param {string} eventName
* @param {...string} additionalEventNames
*/
export function Off(eventName, ...additionalEventNames) {
let eventsToRemove = [eventName, ...additionalEventNames];
eventsToRemove.forEach(eventName => {
eventListeners.delete(eventName);
})
}
/**
* OffAll unregisters all listeners
* [v3 CHANGE] OffAll only unregisters listeners within the current window
*
*/
export function OffAll() {
eventListeners.clear();
}
/*
Emit emits an event to all listeners
*/
export function Emit(event) {
call("Emit", event);
}

View File

@ -0,0 +1,115 @@
import { On, Off, OffAll, OnMultiple, CustomEvent, dispatchCustomEvent, eventListeners, Once } from './events'
import { expect, describe, it, vi, afterEach, beforeEach } from 'vitest'
afterEach(() => {
OffAll();
vi.resetAllMocks()
})
describe('OnMultiple', () => {
let testEvent = new CustomEvent('a', {})
it('should stop after a specified number of times', () => {
const cb = vi.fn()
OnMultiple('a', cb, 5)
dispatchCustomEvent(testEvent)
dispatchCustomEvent(testEvent)
dispatchCustomEvent(testEvent)
dispatchCustomEvent(testEvent)
dispatchCustomEvent(testEvent)
dispatchCustomEvent(testEvent)
expect(cb).toBeCalledTimes(5);
})
it('should return a cancel fn', () => {
const cb = vi.fn()
const cancel = OnMultiple('a', cb, 5)
dispatchCustomEvent(testEvent)
dispatchCustomEvent(testEvent)
cancel()
dispatchCustomEvent(testEvent)
dispatchCustomEvent(testEvent)
expect(cb).toBeCalledTimes(2)
})
})
describe('On', () => {
it('should create a listener with a count of -1', () => {
On('a', () => {})
expect(eventListeners.get("a")[0].maxCallbacks).toBe(-1)
})
it('should return a cancel fn', () => {
const cancel = On('a', () => {})
cancel();
})
})
describe('Once', () => {
it('should create a listener with a count of 1', () => {
Once('a', () => {})
expect(eventListeners.get("a")[0].maxCallbacks).toBe(1)
})
it('should return a cancel fn', () => {
const cancel = EventsOn('a', () => {})
cancel();
})
})
//
// describe('EventsNotify', () => {
// it('should inform a listener', () => {
// const cb = vi.fn()
// EventsOn('a', cb)
// EventsNotify(JSON.stringify({name: 'a', data: ["one", "two", "three"]}))
// expect(cb).toBeCalledTimes(1);
// expect(cb).toHaveBeenLastCalledWith("one", "two", "three");
// expect(window.WailsInvoke).toBeCalledTimes(0);
// })
// })
//
// describe('EventsEmit', () => {
// it('should emit an event', () => {
// EventsEmit('a', 'one', 'two', 'three')
// expect(window.WailsInvoke).toBeCalledTimes(1);
// const calledWith = window.WailsInvoke.calls[0][0];
// expect(calledWith.slice(0, 2)).toBe('EE')
// expect(JSON.parse(calledWith.slice(2))).toStrictEqual({data: ["one", "two", "three"], name: "a"})
// })
// })
//
describe('Off', () => {
beforeEach(() => {
On('a', () => {})
On('a', () => {})
On('a', () => {})
On('b', () => {})
On('c', () => {})
})
it('should cancel all event listeners for a single type', () => {
Off('a')
expect(eventListeners.get('a')).toBeUndefined()
expect(eventListeners.get('b')).not.toBeUndefined()
expect(eventListeners.get('c')).not.toBeUndefined()
})
it('should cancel all event listeners for multiple types', () => {
Off('a', 'b')
expect(eventListeners.get('a')).toBeUndefined()
expect(eventListeners.get('b')).toBeUndefined()
expect(eventListeners.get('c')).not.toBeUndefined()
})
})
describe('OffAll', () => {
it('should cancel all event listeners', () => {
On('a', () => {})
On('a', () => {})
On('a', () => {})
On('b', () => {})
On('c', () => {})
OffAll()
expect(eventListeners.size).toBe(0)
})
})

View File

@ -13,6 +13,7 @@ import {Info, Warning, Error, Question, OpenFile, SaveFile, dialogCallback, dial
import * as Clipboard from './clipboard';
import {newWindow} from "./window";
import {dispatchCustomEvent, Emit, On, Off, OffAll, Once, OnMultiple} from "./events";
// Internal wails endpoints
window.wails = {
@ -22,6 +23,7 @@ window.wails = {
window._wails = {
dialogCallback,
dialogErrorCallback,
dispatchCustomEvent,
}
@ -38,6 +40,14 @@ export function newRuntime(id) {
OpenFile,
SaveFile,
},
Events: {
Emit,
On,
Once,
OnMultiple,
Off,
OffAll,
},
Window: newWindow(id),
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
(()=>{var T=Object.defineProperty;var F=(n,e)=>{for(var t in e)T(n,t,{get:e[t],enumerable:!0})};var b=window.location.origin+"/wails/runtime";function m(n,e){let t=new URL(b);return t.searchParams.append("method",n),t.searchParams.append("args",JSON.stringify(e)),new Promise((i,o)=>{fetch(t).then(r=>{if(r.ok)return r.headers.get("content-type")&&r.headers.get("content-type").indexOf("application/json")!==-1?r.json():r.text();o(Error(r.statusText))}).then(r=>i(r)).catch(r=>o(r))})}function a(n,e){return!e||e===-1?function(t,i){return i=i||{},m(n+"."+t,i)}:function(t,i){return i=i||{},i.windowID=e,m(n+"."+t,i)}}var O="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";var s=(n=21)=>{let e="",t=n;for(;t--;)e+=O[Math.random()*64|0];return e};var U=a("dialog"),l=new Map;function z(){let n;do n=s();while(l.has(n));return n}function d(n,e,t){let i=l.get(n);i&&(t?i.resolve(JSON.parse(e)):i.resolve(e),l.delete(n))}function f(n,e){let t=l.get(n);t&&(t.reject(e),l.delete(n))}function u(n,e){return new Promise((t,i)=>{let o=z();e=e||{},e["dialog-id"]=o,l.set(o,{resolve:t,reject:i}),U(n,e).catch(r=>{i(r),l.delete(o)})})}function p(n){return u("Info",n)}function w(n){return u("Warning",n)}function S(n){return u("Error",n)}function x(n){return u("Question",n)}function g(n){return u("OpenFile",n)}function h(n){return u("SaveFile",n)}var c={};F(c,{SetText:()=>E,Text:()=>P});var M=a("clipboard");function E(n){return M("SetText",{text:n})}function P(){return M("Text")}function C(n){let e=a("window",n);return{Center:()=>e("Center"),SetTitle:t=>e("SetTitle",{title:t}),Fullscreen:()=>e("Fullscreen"),UnFullscreen:()=>e("UnFullscreen"),SetSize:(t,i)=>e("SetSize",{width:t,height:i}),Size:()=>e("Size"),SetMaxSize:(t,i)=>e("SetMaxSize",{width:t,height:i}),SetMinSize:(t,i)=>e("SetMinSize",{width:t,height:i}),SetAlwaysOnTop:t=>e("SetAlwaysOnTop",{alwaysOnTop:t}),SetPosition:(t,i)=>e("SetPosition",{x:t,y:i}),Position:()=>e("Position"),Screen:()=>e("Screen"),Hide:()=>e("Hide"),Maximise:()=>e("Maximise"),Show:()=>e("Show"),ToggleMaximise:()=>e("ToggleMaximise"),UnMaximise:()=>e("UnMaximise"),Minimise:()=>e("Minimise"),UnMinimise:()=>e("UnMinimise"),SetBackgroundColour:(t,i,o,r)=>e("SetBackgroundColour",{r:t,g:i,b:o,a:r})}}window.wails={...R(-1)};window._wails={dialogCallback:d,dialogErrorCallback:f};function R(n){return{Clipboard:{...c},Dialog:{Info:p,Warning:w,Error:S,Question:x,OpenFile:g,SaveFile:h},Window:C(n)}}console.log("Wails v3.0.0 Debug Mode Enabled");})();
(()=>{var z=Object.defineProperty;var P=(t,e)=>{for(var n in e)z(t,n,{get:e[n],enumerable:!0})};var W=window.location.origin+"/wails/runtime";function p(t,e){let n=new URL(W);return n.searchParams.append("method",t),n.searchParams.append("args",JSON.stringify(e)),new Promise((i,r)=>{fetch(n).then(o=>{if(o.ok)return o.headers.get("content-type")&&o.headers.get("content-type").indexOf("application/json")!==-1?o.json():o.text();r(Error(o.statusText))}).then(o=>i(o)).catch(o=>r(o))})}function a(t,e){return!e||e===-1?function(n,i){return i=i||{},p(t+"."+n,i)}:function(n,i){return i=i||{},i.windowID=e,p(t+"."+n,i)}}var y="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";var d=(t=21)=>{let e="",n=t;for(;n--;)e+=y[Math.random()*64|0];return e};var A=a("dialog"),u=new Map;function D(){let t;do t=d();while(u.has(t));return t}function x(t,e,n){let i=u.get(t);i&&(n?i.resolve(JSON.parse(e)):i.resolve(e),u.delete(t))}function h(t,e){let n=u.get(t);n&&(n.reject(e),u.delete(t))}function s(t,e){return new Promise((n,i)=>{let r=D();e=e||{},e["dialog-id"]=r,u.set(r,{resolve:n,reject:i}),A(t,e).catch(o=>{i(o),u.delete(r)})})}function w(t){return s("Info",t)}function S(t){return s("Warning",t)}function g(t){return s("Error",t)}function C(t){return s("Question",t)}function M(t){return s("OpenFile",t)}function O(t){return s("SaveFile",t)}var f={};P(f,{SetText:()=>I,Text:()=>B});var b=a("clipboard");function I(t){return b("SetText",{text:t})}function B(){return b("Text")}function v(t){let e=a("window",t);return{Center:()=>e("Center"),SetTitle:n=>e("SetTitle",{title:n}),Fullscreen:()=>e("Fullscreen"),UnFullscreen:()=>e("UnFullscreen"),SetSize:(n,i)=>e("SetSize",{width:n,height:i}),Size:()=>e("Size"),SetMaxSize:(n,i)=>e("SetMaxSize",{width:n,height:i}),SetMinSize:(n,i)=>e("SetMinSize",{width:n,height:i}),SetAlwaysOnTop:n=>e("SetAlwaysOnTop",{alwaysOnTop:n}),SetPosition:(n,i)=>e("SetPosition",{x:n,y:i}),Position:()=>e("Position"),Screen:()=>e("Screen"),Hide:()=>e("Hide"),Maximise:()=>e("Maximise"),Show:()=>e("Show"),ToggleMaximise:()=>e("ToggleMaximise"),UnMaximise:()=>e("UnMaximise"),Minimise:()=>e("Minimise"),UnMinimise:()=>e("UnMinimise"),SetBackgroundColour:(n,i,r,o)=>e("SetBackgroundColour",{r:n,g:i,b:r,a:o})}}var L=a("events"),m=class{constructor(e,n,i){this.eventName=e,this.maxCallbacks=i||-1,this.Callback=r=>(n(r),this.maxCallbacks===-1?!1:(this.maxCallbacks-=1,this.maxCallbacks===0))}};var l=new Map;function c(t,e,n){let i=l.get(t)||[],r=new m(t,e,n);return i.push(r),l.set(t,i),()=>N(r)}function E(t,e){return c(t,e,-1)}function T(t,e){return c(t,e,1)}function N(t){let e=t.eventName,n=l.get(e).filter(i=>i!==t);n.length===0?l.delete(e):l.set(e,n)}function k(t){console.log("dispatching event: ",{event:t});let e=l.get(t.name);if(e){let n=[];e.forEach(i=>{i.Callback(t)&&n.push(i)}),n.length>0&&(e=e.filter(i=>!n.includes(i)),e.length===0?l.delete(t.name):l.set(t.name,e))}}function R(t,...e){[t,...e].forEach(i=>{l.delete(i)})}function F(){l.clear()}function U(t){L("Emit",t)}window.wails={...Q(-1)};window._wails={dialogCallback:x,dialogErrorCallback:h,dispatchCustomEvent:k};function Q(t){return{Clipboard:{...f},Dialog:{Info:w,Warning:S,Error:g,Question:C,OpenFile:M,SaveFile:O},Events:{Emit:U,On:E,Once:T,OnMultiple:c,Off:R,OffAll:F},Window:v(t)}}console.log("Wails v3.0.0 Debug Mode Enabled");})();

View File

@ -1 +1 @@
(()=>{var T=Object.defineProperty;var F=(n,e)=>{for(var t in e)T(n,t,{get:e[t],enumerable:!0})};var b=window.location.origin+"/wails/runtime";function m(n,e){let t=new URL(b);return t.searchParams.append("method",n),t.searchParams.append("args",JSON.stringify(e)),new Promise((i,o)=>{fetch(t).then(r=>{if(r.ok)return r.headers.get("content-type")&&r.headers.get("content-type").indexOf("application/json")!==-1?r.json():r.text();o(Error(r.statusText))}).then(r=>i(r)).catch(r=>o(r))})}function a(n,e){return!e||e===-1?function(t,i){return i=i||{},m(n+"."+t,i)}:function(t,i){return i=i||{},i.windowID=e,m(n+"."+t,i)}}var O="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";var s=(n=21)=>{let e="",t=n;for(;t--;)e+=O[Math.random()*64|0];return e};var U=a("dialog"),l=new Map;function z(){let n;do n=s();while(l.has(n));return n}function d(n,e,t){let i=l.get(n);i&&(t?i.resolve(JSON.parse(e)):i.resolve(e),l.delete(n))}function f(n,e){let t=l.get(n);t&&(t.reject(e),l.delete(n))}function u(n,e){return new Promise((t,i)=>{let o=z();e=e||{},e["dialog-id"]=o,l.set(o,{resolve:t,reject:i}),U(n,e).catch(r=>{i(r),l.delete(o)})})}function p(n){return u("Info",n)}function w(n){return u("Warning",n)}function S(n){return u("Error",n)}function x(n){return u("Question",n)}function g(n){return u("OpenFile",n)}function h(n){return u("SaveFile",n)}var c={};F(c,{SetText:()=>E,Text:()=>P});var M=a("clipboard");function E(n){return M("SetText",{text:n})}function P(){return M("Text")}function C(n){let e=a("window",n);return{Center:()=>e("Center"),SetTitle:t=>e("SetTitle",{title:t}),Fullscreen:()=>e("Fullscreen"),UnFullscreen:()=>e("UnFullscreen"),SetSize:(t,i)=>e("SetSize",{width:t,height:i}),Size:()=>e("Size"),SetMaxSize:(t,i)=>e("SetMaxSize",{width:t,height:i}),SetMinSize:(t,i)=>e("SetMinSize",{width:t,height:i}),SetAlwaysOnTop:t=>e("SetAlwaysOnTop",{alwaysOnTop:t}),SetPosition:(t,i)=>e("SetPosition",{x:t,y:i}),Position:()=>e("Position"),Screen:()=>e("Screen"),Hide:()=>e("Hide"),Maximise:()=>e("Maximise"),Show:()=>e("Show"),ToggleMaximise:()=>e("ToggleMaximise"),UnMaximise:()=>e("UnMaximise"),Minimise:()=>e("Minimise"),UnMinimise:()=>e("UnMinimise"),SetBackgroundColour:(t,i,o,r)=>e("SetBackgroundColour",{r:t,g:i,b:o,a:r})}}window.wails={...R(-1)};window._wails={dialogCallback:d,dialogErrorCallback:f};function R(n){return{Clipboard:{...c},Dialog:{Info:p,Warning:w,Error:S,Question:x,OpenFile:g,SaveFile:h},Window:C(n)}}console.log("Wails v3.0.0 Debug Mode Enabled");})();
(()=>{var z=Object.defineProperty;var P=(t,e)=>{for(var n in e)z(t,n,{get:e[n],enumerable:!0})};var W=window.location.origin+"/wails/runtime";function p(t,e){let n=new URL(W);return n.searchParams.append("method",t),n.searchParams.append("args",JSON.stringify(e)),new Promise((i,r)=>{fetch(n).then(o=>{if(o.ok)return o.headers.get("content-type")&&o.headers.get("content-type").indexOf("application/json")!==-1?o.json():o.text();r(Error(o.statusText))}).then(o=>i(o)).catch(o=>r(o))})}function a(t,e){return!e||e===-1?function(n,i){return i=i||{},p(t+"."+n,i)}:function(n,i){return i=i||{},i.windowID=e,p(t+"."+n,i)}}var y="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";var d=(t=21)=>{let e="",n=t;for(;n--;)e+=y[Math.random()*64|0];return e};var A=a("dialog"),u=new Map;function D(){let t;do t=d();while(u.has(t));return t}function x(t,e,n){let i=u.get(t);i&&(n?i.resolve(JSON.parse(e)):i.resolve(e),u.delete(t))}function h(t,e){let n=u.get(t);n&&(n.reject(e),u.delete(t))}function s(t,e){return new Promise((n,i)=>{let r=D();e=e||{},e["dialog-id"]=r,u.set(r,{resolve:n,reject:i}),A(t,e).catch(o=>{i(o),u.delete(r)})})}function w(t){return s("Info",t)}function S(t){return s("Warning",t)}function g(t){return s("Error",t)}function C(t){return s("Question",t)}function M(t){return s("OpenFile",t)}function O(t){return s("SaveFile",t)}var f={};P(f,{SetText:()=>I,Text:()=>B});var b=a("clipboard");function I(t){return b("SetText",{text:t})}function B(){return b("Text")}function v(t){let e=a("window",t);return{Center:()=>e("Center"),SetTitle:n=>e("SetTitle",{title:n}),Fullscreen:()=>e("Fullscreen"),UnFullscreen:()=>e("UnFullscreen"),SetSize:(n,i)=>e("SetSize",{width:n,height:i}),Size:()=>e("Size"),SetMaxSize:(n,i)=>e("SetMaxSize",{width:n,height:i}),SetMinSize:(n,i)=>e("SetMinSize",{width:n,height:i}),SetAlwaysOnTop:n=>e("SetAlwaysOnTop",{alwaysOnTop:n}),SetPosition:(n,i)=>e("SetPosition",{x:n,y:i}),Position:()=>e("Position"),Screen:()=>e("Screen"),Hide:()=>e("Hide"),Maximise:()=>e("Maximise"),Show:()=>e("Show"),ToggleMaximise:()=>e("ToggleMaximise"),UnMaximise:()=>e("UnMaximise"),Minimise:()=>e("Minimise"),UnMinimise:()=>e("UnMinimise"),SetBackgroundColour:(n,i,r,o)=>e("SetBackgroundColour",{r:n,g:i,b:r,a:o})}}var L=a("events"),m=class{constructor(e,n,i){this.eventName=e,this.maxCallbacks=i||-1,this.Callback=r=>(n(r),this.maxCallbacks===-1?!1:(this.maxCallbacks-=1,this.maxCallbacks===0))}};var l=new Map;function c(t,e,n){let i=l.get(t)||[],r=new m(t,e,n);return i.push(r),l.set(t,i),()=>N(r)}function E(t,e){return c(t,e,-1)}function T(t,e){return c(t,e,1)}function N(t){let e=t.eventName,n=l.get(e).filter(i=>i!==t);n.length===0?l.delete(e):l.set(e,n)}function k(t){console.log("dispatching event: ",{event:t});let e=l.get(t.name);if(e){let n=[];e.forEach(i=>{i.Callback(t)&&n.push(i)}),n.length>0&&(e=e.filter(i=>!n.includes(i)),e.length===0?l.delete(t.name):l.set(t.name,e))}}function R(t,...e){[t,...e].forEach(i=>{l.delete(i)})}function F(){l.clear()}function U(t){L("Emit",t)}window.wails={...Q(-1)};window._wails={dialogCallback:x,dialogErrorCallback:h,dispatchCustomEvent:k};function Q(t){return{Clipboard:{...f},Dialog:{Info:w,Warning:S,Error:g,Question:C,OpenFile:M,SaveFile:O},Events:{Emit:U,On:E,Once:T,OnMultiple:c,Off:R,OffAll:F},Window:v(t)}}console.log("Wails v3.0.0 Debug Mode Enabled");})();

View File

@ -1 +1 @@
(()=>{var T=Object.defineProperty;var F=(n,e)=>{for(var t in e)T(n,t,{get:e[t],enumerable:!0})};var b=window.location.origin+"/wails/runtime";function m(n,e){let t=new URL(b);return t.searchParams.append("method",n),t.searchParams.append("args",JSON.stringify(e)),new Promise((i,o)=>{fetch(t).then(r=>{if(r.ok)return r.headers.get("content-type")&&r.headers.get("content-type").indexOf("application/json")!==-1?r.json():r.text();o(Error(r.statusText))}).then(r=>i(r)).catch(r=>o(r))})}function a(n,e){return!e||e===-1?function(t,i){return i=i||{},m(n+"."+t,i)}:function(t,i){return i=i||{},i.windowID=e,m(n+"."+t,i)}}var O="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";var s=(n=21)=>{let e="",t=n;for(;t--;)e+=O[Math.random()*64|0];return e};var U=a("dialog"),l=new Map;function z(){let n;do n=s();while(l.has(n));return n}function d(n,e,t){let i=l.get(n);i&&(t?i.resolve(JSON.parse(e)):i.resolve(e),l.delete(n))}function f(n,e){let t=l.get(n);t&&(t.reject(e),l.delete(n))}function u(n,e){return new Promise((t,i)=>{let o=z();e=e||{},e["dialog-id"]=o,l.set(o,{resolve:t,reject:i}),U(n,e).catch(r=>{i(r),l.delete(o)})})}function p(n){return u("Info",n)}function w(n){return u("Warning",n)}function S(n){return u("Error",n)}function x(n){return u("Question",n)}function g(n){return u("OpenFile",n)}function h(n){return u("SaveFile",n)}var c={};F(c,{SetText:()=>E,Text:()=>P});var M=a("clipboard");function E(n){return M("SetText",{text:n})}function P(){return M("Text")}function C(n){let e=a("window",n);return{Center:()=>e("Center"),SetTitle:t=>e("SetTitle",{title:t}),Fullscreen:()=>e("Fullscreen"),UnFullscreen:()=>e("UnFullscreen"),SetSize:(t,i)=>e("SetSize",{width:t,height:i}),Size:()=>e("Size"),SetMaxSize:(t,i)=>e("SetMaxSize",{width:t,height:i}),SetMinSize:(t,i)=>e("SetMinSize",{width:t,height:i}),SetAlwaysOnTop:t=>e("SetAlwaysOnTop",{alwaysOnTop:t}),SetPosition:(t,i)=>e("SetPosition",{x:t,y:i}),Position:()=>e("Position"),Screen:()=>e("Screen"),Hide:()=>e("Hide"),Maximise:()=>e("Maximise"),Show:()=>e("Show"),ToggleMaximise:()=>e("ToggleMaximise"),UnMaximise:()=>e("UnMaximise"),Minimise:()=>e("Minimise"),UnMinimise:()=>e("UnMinimise"),SetBackgroundColour:(t,i,o,r)=>e("SetBackgroundColour",{r:t,g:i,b:o,a:r})}}window.wails={...R(-1)};window._wails={dialogCallback:d,dialogErrorCallback:f};function R(n){return{Clipboard:{...c},Dialog:{Info:p,Warning:w,Error:S,Question:x,OpenFile:g,SaveFile:h},Window:C(n)}}console.log("Wails v3.0.0 Debug Mode Enabled");})();
(()=>{var z=Object.defineProperty;var P=(t,e)=>{for(var n in e)z(t,n,{get:e[n],enumerable:!0})};var W=window.location.origin+"/wails/runtime";function p(t,e){let n=new URL(W);return n.searchParams.append("method",t),n.searchParams.append("args",JSON.stringify(e)),new Promise((i,r)=>{fetch(n).then(o=>{if(o.ok)return o.headers.get("content-type")&&o.headers.get("content-type").indexOf("application/json")!==-1?o.json():o.text();r(Error(o.statusText))}).then(o=>i(o)).catch(o=>r(o))})}function a(t,e){return!e||e===-1?function(n,i){return i=i||{},p(t+"."+n,i)}:function(n,i){return i=i||{},i.windowID=e,p(t+"."+n,i)}}var y="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";var d=(t=21)=>{let e="",n=t;for(;n--;)e+=y[Math.random()*64|0];return e};var A=a("dialog"),u=new Map;function D(){let t;do t=d();while(u.has(t));return t}function x(t,e,n){let i=u.get(t);i&&(n?i.resolve(JSON.parse(e)):i.resolve(e),u.delete(t))}function h(t,e){let n=u.get(t);n&&(n.reject(e),u.delete(t))}function s(t,e){return new Promise((n,i)=>{let r=D();e=e||{},e["dialog-id"]=r,u.set(r,{resolve:n,reject:i}),A(t,e).catch(o=>{i(o),u.delete(r)})})}function w(t){return s("Info",t)}function S(t){return s("Warning",t)}function g(t){return s("Error",t)}function C(t){return s("Question",t)}function M(t){return s("OpenFile",t)}function O(t){return s("SaveFile",t)}var f={};P(f,{SetText:()=>I,Text:()=>B});var b=a("clipboard");function I(t){return b("SetText",{text:t})}function B(){return b("Text")}function v(t){let e=a("window",t);return{Center:()=>e("Center"),SetTitle:n=>e("SetTitle",{title:n}),Fullscreen:()=>e("Fullscreen"),UnFullscreen:()=>e("UnFullscreen"),SetSize:(n,i)=>e("SetSize",{width:n,height:i}),Size:()=>e("Size"),SetMaxSize:(n,i)=>e("SetMaxSize",{width:n,height:i}),SetMinSize:(n,i)=>e("SetMinSize",{width:n,height:i}),SetAlwaysOnTop:n=>e("SetAlwaysOnTop",{alwaysOnTop:n}),SetPosition:(n,i)=>e("SetPosition",{x:n,y:i}),Position:()=>e("Position"),Screen:()=>e("Screen"),Hide:()=>e("Hide"),Maximise:()=>e("Maximise"),Show:()=>e("Show"),ToggleMaximise:()=>e("ToggleMaximise"),UnMaximise:()=>e("UnMaximise"),Minimise:()=>e("Minimise"),UnMinimise:()=>e("UnMinimise"),SetBackgroundColour:(n,i,r,o)=>e("SetBackgroundColour",{r:n,g:i,b:r,a:o})}}var L=a("events"),m=class{constructor(e,n,i){this.eventName=e,this.maxCallbacks=i||-1,this.Callback=r=>(n(r),this.maxCallbacks===-1?!1:(this.maxCallbacks-=1,this.maxCallbacks===0))}};var l=new Map;function c(t,e,n){let i=l.get(t)||[],r=new m(t,e,n);return i.push(r),l.set(t,i),()=>N(r)}function E(t,e){return c(t,e,-1)}function T(t,e){return c(t,e,1)}function N(t){let e=t.eventName,n=l.get(e).filter(i=>i!==t);n.length===0?l.delete(e):l.set(e,n)}function k(t){console.log("dispatching event: ",{event:t});let e=l.get(t.name);if(e){let n=[];e.forEach(i=>{i.Callback(t)&&n.push(i)}),n.length>0&&(e=e.filter(i=>!n.includes(i)),e.length===0?l.delete(t.name):l.set(t.name,e))}}function R(t,...e){[t,...e].forEach(i=>{l.delete(i)})}function F(){l.clear()}function U(t){L("Emit",t)}window.wails={...Q(-1)};window._wails={dialogCallback:x,dialogErrorCallback:h,dispatchCustomEvent:k};function Q(t){return{Clipboard:{...f},Dialog:{Info:w,Warning:S,Error:g,Question:C,OpenFile:M,SaveFile:O},Events:{Emit:U,On:E,Once:T,OnMultiple:c,Off:R,OffAll:F},Window:v(t)}}console.log("Wails v3.0.0 Debug Mode Enabled");})();

View File

@ -30,6 +30,7 @@ func New(appOptions options.Application) *App {
applicationEventListeners: make(map[uint][]func()),
systemTrays: make(map[uint]*SystemTray),
}
result.Events = NewCustomEventProcessor(result.dispatchEventToWindows)
globalApplication = result
return result
}
@ -74,6 +75,13 @@ type webViewAssetRequest struct {
var webviewRequests = make(chan *webViewAssetRequest)
type EventHandler interface {
On(eventName string, args any)
OnMultiple(eventName string, args any, count int)
Once(eventName string, args any)
Emit(eventName string, args any)
}
type App struct {
options options.Application
applicationEventListeners map[uint][]func()
@ -106,6 +114,8 @@ type App struct {
// About MessageDialog
clipboard *Clipboard
Events *EventProcessor
}
func (a *App) getSystemTrayID() uint {
@ -127,7 +137,7 @@ func (a *App) On(eventType events.ApplicationEventType, callback func()) {
defer a.applicationEventListenersLock.Unlock()
a.applicationEventListeners[eventID] = append(a.applicationEventListeners[eventID], callback)
if a.impl != nil {
a.impl.on(eventID)
go a.impl.on(eventID)
}
}
func (a *App) NewWebviewWindow() *WebviewWindow {
@ -401,3 +411,9 @@ func (a *App) SaveFileDialogWithOptions(s *SaveFileDialogOptions) *SaveFileDialo
result.SetOptions(s)
return result
}
func (a *App) dispatchEventToWindows(event *CustomEvent) {
for _, window := range a.windows {
window.dispatchCustomEvent(event)
}
}

View File

@ -1,5 +1,12 @@
package application
import (
"encoding/json"
"sync"
"github.com/samber/lo"
)
var applicationEvents = make(chan uint)
type WindowEvent struct {
@ -10,3 +17,144 @@ type WindowEvent struct {
var windowEvents = make(chan *WindowEvent)
var menuItemClicked = make(chan uint)
type CustomEvent struct {
Name string `json:"name"`
Data any `json:"data"`
Sender string `json:"sender"`
}
func (e CustomEvent) ToJSON() string {
marshal, err := json.Marshal(&e)
if err != nil {
// TODO: Fatal error? log?
return ""
}
return string(marshal)
}
// 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(*CustomEvent) // 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
}
// EventProcessor handles custom events
type EventProcessor struct {
// Go event listeners
listeners map[string][]*eventListener
notifyLock sync.RWMutex
dispatchEventToWindows func(*CustomEvent)
}
func NewCustomEventProcessor(dispatchEventToWindows func(*CustomEvent)) *EventProcessor {
return &EventProcessor{
listeners: make(map[string][]*eventListener),
dispatchEventToWindows: dispatchEventToWindows,
}
}
// On is the equivalent of Javascript's `addEventListener`
func (e *EventProcessor) On(eventName string, callback func(event *CustomEvent)) func() {
return e.registerListener(eventName, callback, -1)
}
// OnMultiple is the same as `On` but will unregister after `count` events
func (e *EventProcessor) OnMultiple(eventName string, callback func(event *CustomEvent), counter int) func() {
return e.registerListener(eventName, callback, counter)
}
// Once is the same as `On` but will unregister after the first event
func (e *EventProcessor) Once(eventName string, callback func(event *CustomEvent)) func() {
return e.registerListener(eventName, callback, 1)
}
// Emit sends an event to all listeners
func (e *EventProcessor) Emit(thisEvent *CustomEvent) {
if thisEvent == nil {
return
}
go e.dispatchEventToListeners(thisEvent)
go e.dispatchEventToWindows(thisEvent)
}
func (e *EventProcessor) Off(eventName string) {
e.unRegisterListener(eventName)
}
func (e *EventProcessor) OffAll() {
e.notifyLock.Lock()
defer e.notifyLock.Unlock()
e.listeners = make(map[string][]*eventListener)
}
// registerListener provides a means of subscribing to events of type "eventName"
func (e *EventProcessor) registerListener(eventName string, callback func(*CustomEvent), counter int) func() {
// 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()
return func() {
e.notifyLock.Lock()
defer e.notifyLock.Unlock()
if _, ok := e.listeners[eventName]; !ok {
return
}
e.listeners[eventName] = lo.Filter(e.listeners[eventName], func(l *eventListener, i int) bool {
return l != thisListener
})
}
}
// unRegisterListener provides a means of unsubscribing to events of type "eventName"
func (e *EventProcessor) unRegisterListener(eventName string) {
e.notifyLock.Lock()
defer e.notifyLock.Unlock()
delete(e.listeners, eventName)
}
// dispatchEventToListeners calls all registered listeners event name
func (e *EventProcessor) dispatchEventToListeners(event *CustomEvent) {
e.notifyLock.Lock()
defer e.notifyLock.Unlock()
listeners := e.listeners[event.Name]
if listeners == nil {
return
}
// 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(event)
if listener.counter == 0 {
listener.delete = true
itemsToDelete = true
}
}
// Do we have items to delete?
if itemsToDelete == true {
e.listeners[event.Name] = lo.Filter(listeners, func(l *eventListener, i int) bool {
return l.delete == false
})
}
}

View File

@ -0,0 +1,135 @@
package application_test
import (
"sync"
"testing"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/matryer/is"
)
type mockNotifier struct {
Events []*application.CustomEvent
}
func (m *mockNotifier) dispatchEventToWindows(event *application.CustomEvent) {
m.Events = append(m.Events, event)
}
func (m *mockNotifier) Reset() {
m.Events = []*application.CustomEvent{}
}
func Test_EventsOn(t *testing.T) {
i := is.New(t)
notifier := &mockNotifier{}
eventProcessor := application.NewCustomEventProcessor(notifier.dispatchEventToWindows)
// Test On
eventName := "test"
counter := 0
var wg sync.WaitGroup
wg.Add(1)
unregisterFn := eventProcessor.On(eventName, func(event *application.CustomEvent) {
// This is called in a goroutine
counter++
wg.Done()
})
eventProcessor.Emit(&application.CustomEvent{
Name: "test",
Data: "test payload",
})
wg.Wait()
i.Equal(1, counter)
// Unregister
notifier.Reset()
unregisterFn()
counter = 0
eventProcessor.Emit(&application.CustomEvent{
Name: "test",
Data: "test payload",
})
i.Equal(0, counter)
}
func Test_EventsOnce(t *testing.T) {
i := is.New(t)
notifier := &mockNotifier{}
eventProcessor := application.NewCustomEventProcessor(notifier.dispatchEventToWindows)
// Test On
eventName := "test"
counter := 0
var wg sync.WaitGroup
wg.Add(1)
unregisterFn := eventProcessor.Once(eventName, func(event *application.CustomEvent) {
// This is called in a goroutine
counter++
wg.Done()
})
eventProcessor.Emit(&application.CustomEvent{
Name: "test",
Data: "test payload",
})
eventProcessor.Emit(&application.CustomEvent{
Name: "test",
Data: "test payload",
})
wg.Wait()
i.Equal(1, counter)
// Unregister
notifier.Reset()
unregisterFn()
counter = 0
eventProcessor.Emit(&application.CustomEvent{
Name: "test",
Data: "test payload",
})
i.Equal(0, counter)
}
func Test_EventsOnMultiple(t *testing.T) {
i := is.New(t)
notifier := &mockNotifier{}
eventProcessor := application.NewCustomEventProcessor(notifier.dispatchEventToWindows)
// Test On
eventName := "test"
counter := 0
var wg sync.WaitGroup
wg.Add(2)
unregisterFn := eventProcessor.OnMultiple(eventName, func(event *application.CustomEvent) {
// This is called in a goroutine
counter++
wg.Done()
}, 2)
eventProcessor.Emit(&application.CustomEvent{
Name: "test",
Data: "test payload",
})
eventProcessor.Emit(&application.CustomEvent{
Name: "test",
Data: "test payload",
})
eventProcessor.Emit(&application.CustomEvent{
Name: "test",
Data: "test payload",
})
wg.Wait()
i.Equal(2, counter)
// Unregister
notifier.Reset()
unregisterFn()
counter = 0
eventProcessor.Emit(&application.CustomEvent{
Name: "test",
Data: "test payload",
})
i.Equal(0, counter)
}

View File

@ -29,7 +29,7 @@ func dispatchOnMainThreadCallback(callbackID C.uint) {
id := uint(callbackID)
fn := mainThreadFunctionStore[id]
if fn == nil {
Fatal("dispatchCallback called with invalid id: ", id)
Fatal("dispatchCallback called with invalid id: %v", id)
}
delete(mainThreadFunctionStore, id)
mainThreadFunctionStoreLock.RUnlock()

View File

@ -61,6 +61,8 @@ func (m *MessageProcessor) HandleRuntimeCall(rw http.ResponseWriter, r *http.Req
m.processClipboardMethod(method, rw, r, targetWindow, params)
case "dialog":
m.processDialogMethod(method, rw, r, targetWindow, params)
case "events":
m.processEventsMethod(method, rw, r, targetWindow, params)
default:
m.httpError(rw, "Unknown runtime call: %s", object)
}

View File

@ -0,0 +1,29 @@
package application
import (
"fmt"
"net/http"
)
func (m *MessageProcessor) processEventsMethod(method string, rw http.ResponseWriter, r *http.Request, window *WebviewWindow, params QueryParams) {
switch method {
case "Emit":
var event CustomEvent
err := params.ToStruct(&event)
if err != nil {
m.httpError(rw, "Error parsing event: %s", err)
return
}
if event.Name == "" {
m.httpError(rw, "Event name must be specified")
return
}
event.Sender = fmt.Sprintf("%d", window.id)
globalApplication.Events.Emit(&event)
m.ok(rw)
default:
m.httpError(rw, "Unknown event method: %s", method)
}
}

View File

@ -616,3 +616,8 @@ func (w *WebviewWindow) SetFrameless(frameless bool) *WebviewWindow {
}
return w
}
func (w *WebviewWindow) dispatchCustomEvent(event *CustomEvent) {
msg := fmt.Sprintf("_wails.dispatchCustomEvent(%s);", event.ToJSON())
w.ExecJS(msg)
}