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:
parent
0e8144e52f
commit
f26756be9c
@ -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
53
v3/V3 Changes.md
Normal 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
|
||||
|
21
v3/examples/events/assets/index.html
Normal file
21
v3/examples/events/assets/index.html
Normal 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>
|
72
v3/examples/events/main.go
Normal file
72
v3/examples/events/main.go
Normal 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())
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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=
|
||||
|
187
v3/internal/runtime/desktop/events.js
Normal file
187
v3/internal/runtime/desktop/events.js
Normal 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);
|
||||
}
|
115
v3/internal/runtime/desktop/events.test.js
Normal file
115
v3/internal/runtime/desktop/events.test.js
Normal 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)
|
||||
})
|
||||
})
|
@ -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
@ -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");})();
|
||||
|
@ -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");})();
|
||||
|
@ -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");})();
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
135
v3/pkg/application/events_test.go
Normal file
135
v3/pkg/application/events_test.go
Normal 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)
|
||||
|
||||
}
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
29
v3/pkg/application/messageprocessor_events.go
Normal file
29
v3/pkg/application/messageprocessor_events.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user