From dc865404a9cbb9642aff9c6231d1e88db1f4a3d3 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Wed, 12 Jul 2023 20:31:13 +1000 Subject: [PATCH] [v3] Initial hooks implementation --- v3/examples/events/main.go | 20 +++++- v3/examples/window/main.go | 2 +- v3/pkg/application/application.go | 49 +++++++++++++-- v3/pkg/application/application_darwin.go | 2 +- v3/pkg/application/application_linux.go | 2 +- v3/pkg/application/events.go | 66 ++++++++++++++++++-- v3/pkg/application/systemtray_windows.go | 2 +- v3/pkg/application/webview_window.go | 52 ++++++++++++++- v3/pkg/application/webview_window_windows.go | 4 +- 9 files changed, 179 insertions(+), 20 deletions(-) diff --git a/v3/examples/events/main.go b/v3/examples/events/main.go index 5cf34796c..3d7c38ada 100644 --- a/v3/examples/events/main.go +++ b/v3/examples/events/main.go @@ -32,7 +32,7 @@ func main() { }) // OS specific application events - app.On(events.Mac.ApplicationDidFinishLaunching, func() { + app.On(events.Mac.ApplicationDidFinishLaunching, func(event *application.Event) { for { log.Println("Sending event") app.Events.Emit(&application.WailsEvent{ @@ -44,7 +44,7 @@ func main() { }) // Platform agnostic events - app.On(events.Common.ApplicationStarted, func() { + app.On(events.Common.ApplicationStarted, func(event *application.Event) { println("events.Common.ApplicationStarted fired!") }) @@ -56,7 +56,7 @@ func main() { InvisibleTitleBarHeight: 50, }, }) - app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + win2 := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ Title: "Events Demo", Mac: application.MacWindow{ Backdrop: application.MacBackdropTranslucent, @@ -65,6 +65,20 @@ func main() { }, }) + var cancel bool + + win2.RegisterHook(events.Common.WindowFocus, func(e *application.WindowEvent) { + println("---------------\n[Hook] Window focus!") + cancel = !cancel + if cancel { + e.Cancel() + } + }) + + win2.On(events.Common.WindowFocus, func(e *application.WindowEventContext) { + println("[Event] Window focus!") + }) + err := app.Run() if err != nil { diff --git a/v3/examples/window/main.go b/v3/examples/window/main.go index abf6bb724..8b16ca1a4 100644 --- a/v3/examples/window/main.go +++ b/v3/examples/window/main.go @@ -22,7 +22,7 @@ func main() { ApplicationShouldTerminateAfterLastWindowClosed: true, }, }) - app.On(events.Mac.ApplicationDidFinishLaunching, func() { + app.On(events.Mac.ApplicationDidFinishLaunching, func(event *application.Event) { log.Println("ApplicationDidFinishLaunching") }) diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 25b7ec3f0..0fc56a94d 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -35,7 +35,7 @@ func init() { } type EventListener struct { - callback func() + callback func(*Event) } func Get() *App { @@ -219,10 +219,16 @@ func (r *webViewAssetRequest) Header() (http.Header, error) { var webviewRequests = make(chan *webViewAssetRequest) +type eventHook struct { + callback func(*Event) +} + type App struct { options Options applicationEventListeners map[uint][]*EventListener applicationEventListenersLock sync.RWMutex + applicationEventHooks map[uint][]*eventHook + applicationEventHooksLock sync.RWMutex // Windows windows map[uint]*WebviewWindow @@ -293,7 +299,7 @@ func (a *App) Capabilities() capabilities.Capabilities { return a.capabilities } -func (a *App) On(eventType events.ApplicationEventType, callback func()) func() { +func (a *App) On(eventType events.ApplicationEventType, callback func(event *Event)) func() { eventID := uint(eventType) a.applicationEventListenersLock.Lock() defer a.applicationEventListenersLock.Unlock() @@ -313,6 +319,25 @@ func (a *App) On(eventType events.ApplicationEventType, callback func()) func() a.applicationEventListeners[eventID] = lo.Without(a.applicationEventListeners[eventID], listener) } } + +// RegisterHook registers a hook for the given event type. Hooks are called before the event listeners and can cancel the event. +// The returned function can be called to remove the hook. +func (a *App) RegisterHook(eventType events.ApplicationEventType, callback func(event *Event)) func() { + eventID := uint(eventType) + a.applicationEventHooksLock.Lock() + defer a.applicationEventHooksLock.Unlock() + thisHook := &eventHook{ + callback: callback, + } + a.applicationEventHooks[eventID] = append(a.applicationEventHooks[eventID], thisHook) + + return func() { + a.applicationEventHooksLock.Lock() + a.applicationEventHooks[eventID] = lo.Without(a.applicationEventHooks[eventID], thisHook) + a.applicationEventHooksLock.Unlock() + } +} + func (a *App) NewWebviewWindow() *WebviewWindow { return a.NewWebviewWindowWithOptions(WebviewWindowOptions{}) } @@ -468,8 +493,24 @@ func (a *App) handleApplicationEvent(event uint) { if !ok { return } + + thisEvent := &Event{} + + // Process Hooks + a.applicationEventHooksLock.RLock() + hooks, ok := a.applicationEventHooks[event] + a.applicationEventHooksLock.RUnlock() + if ok { + for _, thisHook := range hooks { + thisHook.callback(thisEvent) + if thisEvent.Cancelled { + return + } + } + } + for _, listener := range listeners { - go listener.callback() + go listener.callback(thisEvent) } } @@ -506,7 +547,7 @@ func (a *App) handleWebViewRequest(request *webViewAssetRequest) { a.assets.ServeWebViewRequest(request) } -func (a *App) handleWindowEvent(event *WindowEvent) { +func (a *App) handleWindowEvent(event *windowEvent) { // Get window from window map a.windowsLock.Lock() window, ok := a.windows[event.WindowID] diff --git a/v3/pkg/application/application_darwin.go b/v3/pkg/application/application_darwin.go index 1ea601874..8e0a0dc15 100644 --- a/v3/pkg/application/application_darwin.go +++ b/v3/pkg/application/application_darwin.go @@ -220,7 +220,7 @@ func processApplicationEvent(eventID C.uint) { //export processWindowEvent func processWindowEvent(windowID C.uint, eventID C.uint) { - windowEvents <- &WindowEvent{ + windowEvents <- &windowEvent{ WindowID: uint(windowID), EventID: uint(eventID), } diff --git a/v3/pkg/application/application_linux.go b/v3/pkg/application/application_linux.go index c5287e8cb..d53e02c94 100644 --- a/v3/pkg/application/application_linux.go +++ b/v3/pkg/application/application_linux.go @@ -130,7 +130,7 @@ func processApplicationEvent(eventID C.uint) { //export processWindowEvent func processWindowEvent(windowID C.uint, eventID C.uint) { - windowEvents <- &WindowEvent{ + windowEvents <- &windowEvent{ WindowID: uint(windowID), EventID: uint(eventID), } diff --git a/v3/pkg/application/events.go b/v3/pkg/application/events.go index 3006e8714..ff85d5dd3 100644 --- a/v3/pkg/application/events.go +++ b/v3/pkg/application/events.go @@ -9,19 +9,32 @@ import ( var applicationEvents = make(chan uint) -type WindowEvent struct { +type windowEvent struct { WindowID uint EventID uint } -var windowEvents = make(chan *WindowEvent) +type Event struct { + Cancelled bool +} + +func (e *Event) Cancel() { + e.Cancelled = true +} + +var windowEvents = make(chan *windowEvent) var menuItemClicked = make(chan uint) type WailsEvent struct { - Name string `json:"name"` - Data any `json:"data"` - Sender string `json:"sender"` + Name string `json:"name"` + Data any `json:"data"` + Sender string `json:"sender"` + Cancelled bool +} + +func (e *WailsEvent) Cancel() { + e.Cancelled = true } var commonEvents = make(chan uint) @@ -35,6 +48,10 @@ func (e WailsEvent) ToJSON() string { return string(marshal) } +type hook struct { + callback func(*WailsEvent) +} + // 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 @@ -51,12 +68,15 @@ type EventProcessor struct { listeners map[string][]*eventListener notifyLock sync.RWMutex dispatchEventToWindows func(*WailsEvent) + hooks map[string][]*hook + hookLock sync.RWMutex } func NewWailsEventProcessor(dispatchEventToWindows func(*WailsEvent)) *EventProcessor { return &EventProcessor{ listeners: make(map[string][]*eventListener), dispatchEventToWindows: dispatchEventToWindows, + hooks: make(map[string][]*hook), } } @@ -80,6 +100,19 @@ func (e *EventProcessor) Emit(thisEvent *WailsEvent) { if thisEvent == nil { return } + + // If we have any hooks, run them first and check if the event was cancelled + if e.hooks != nil { + if hooks, ok := e.hooks[thisEvent.Name]; ok { + for _, thisHook := range hooks { + thisHook.callback(thisEvent) + if thisEvent.Cancelled { + return + } + } + } + } + go e.dispatchEventToListeners(thisEvent) go e.dispatchEventToWindows(thisEvent) } @@ -119,6 +152,29 @@ func (e *EventProcessor) registerListener(eventName string, callback func(*Wails } } +// RegisterHook provides a means of registering methods to be called before emitting the event +func (e *EventProcessor) RegisterHook(eventName string, callback func(*WailsEvent)) func() { + // Create new hook + thisHook := &hook{ + callback: callback, + } + e.hookLock.Lock() + // Append the new listener to the listeners slice + e.hooks[eventName] = append(e.hooks[eventName], thisHook) + e.hookLock.Unlock() + return func() { + e.hookLock.Lock() + defer e.hookLock.Unlock() + + if _, ok := e.hooks[eventName]; !ok { + return + } + e.hooks[eventName] = lo.Filter(e.hooks[eventName], func(l *hook, i int) bool { + return l != thisHook + }) + } +} + // unRegisterListener provides a means of unsubscribing to events of type "eventName" func (e *EventProcessor) unRegisterListener(eventName string) { e.notifyLock.Lock() diff --git a/v3/pkg/application/systemtray_windows.go b/v3/pkg/application/systemtray_windows.go index d0a388a84..4ff7d8bd5 100644 --- a/v3/pkg/application/systemtray_windows.go +++ b/v3/pkg/application/systemtray_windows.go @@ -182,7 +182,7 @@ func (s *windowsSystemTray) run() { s.updateIcon() // Listen for dark mode changes - globalApplication.On(events.Windows.SystemThemeChanged, func() { + globalApplication.On(events.Windows.SystemThemeChanged, func(event *Event) { s.updateIcon() }) diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 333282a3a..cc458193b 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -74,10 +74,23 @@ type ( } ) +type WindowEvent struct { + WindowEventContext + Cancelled bool +} + +func (w *WindowEvent) Cancel() { + w.Cancelled = true +} + type WindowEventListener struct { callback func(ctx *WindowEventContext) } +type windowHook struct { + callback func(ctx *WindowEvent) +} + type WebviewWindow struct { options WebviewWindowOptions impl webviewWindowImpl @@ -85,6 +98,8 @@ type WebviewWindow struct { eventListeners map[uint][]*WindowEventListener eventListenersLock sync.RWMutex + eventHooks map[uint][]*windowHook + eventHooksLock sync.RWMutex contextMenus map[string]*Menu contextMenusLock sync.RWMutex @@ -106,7 +121,7 @@ func getWindowID() uint { // Use onApplicationEvent to register a callback for an application event from a window. // This will handle tidying up the callback when the window is destroyed -func (w *WebviewWindow) onApplicationEvent(eventType events.ApplicationEventType, callback func()) { +func (w *WebviewWindow) onApplicationEvent(eventType events.ApplicationEventType, callback func(*Event)) { cancelFn := globalApplication.On(eventType, callback) w.addCancellationFunction(cancelFn) } @@ -152,6 +167,7 @@ func NewWindow(options WebviewWindowOptions) *WebviewWindow { options: options, eventListeners: make(map[uint][]*WindowEventListener), contextMenus: make(map[string]*Menu), + eventHooks: make(map[uint][]*windowHook), } result.setupEventMapping() @@ -553,12 +569,44 @@ func (w *WebviewWindow) On(eventType events.WindowEventType, callback func(ctx * defer w.eventListenersLock.Unlock() w.eventListeners[eventID] = lo.Without(w.eventListeners[eventID], windowEventListener) } +} +// RegisterHook registers a hook for the given window event +func (w *WebviewWindow) RegisterHook(eventType events.WindowEventType, callback func(ctx *WindowEvent)) func() { + eventID := uint(eventType) + w.eventHooksLock.Lock() + defer w.eventHooksLock.Unlock() + windowEventHook := &windowHook{ + callback: callback, + } + w.eventHooks[eventID] = append(w.eventHooks[eventID], windowEventHook) + + return func() { + w.eventHooksLock.Lock() + defer w.eventHooksLock.Unlock() + w.eventHooks[eventID] = lo.Without(w.eventHooks[eventID], windowEventHook) + } } func (w *WebviewWindow) handleWindowEvent(id uint) { w.eventListenersLock.RLock() defer w.eventListenersLock.RUnlock() + + // Get hooks + w.eventHooksLock.RLock() + hooks := w.eventHooks[id] + w.eventHooksLock.RUnlock() + + // Create new WindowEvent + thisEvent := &WindowEvent{} + + for _, thisHook := range hooks { + thisHook.callback(thisEvent) + if thisEvent.Cancelled { + return + } + } + for _, listener := range w.eventListeners[id] { go listener.callback(blankWindowEventContext) } @@ -932,7 +980,7 @@ func (w *WebviewWindow) Focus() { } func (w *WebviewWindow) emit(eventType events.WindowEventType) { - windowEvents <- &WindowEvent{ + windowEvents <- &windowEvent{ WindowID: w.id, EventID: uint(eventType), } diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index bcaa44433..1af70097e 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -262,7 +262,7 @@ func (w *windowsWebviewWindow) run() { switch options.Windows.Theme { case SystemDefault: w.updateTheme(w32.IsCurrentlyDarkMode()) - w.parent.onApplicationEvent(events.Windows.SystemThemeChanged, func() { + w.parent.onApplicationEvent(events.Windows.SystemThemeChanged, func(*Event) { w.updateTheme(w32.IsCurrentlyDarkMode()) }) case Light: @@ -1475,7 +1475,7 @@ func (w *windowsWebviewWindow) flash(enabled bool) { func (w *windowsWebviewWindow) navigationCompleted(sender *edge.ICoreWebView2, args *edge.ICoreWebView2NavigationCompletedEventArgs) { // Emit DomReady Event - windowEvents <- &WindowEvent{EventID: uint(events.Windows.WebViewNavigationCompleted), WindowID: w.parent.id} + windowEvents <- &windowEvent{EventID: uint(events.Windows.WebViewNavigationCompleted), WindowID: w.parent.id} if w.hasStarted { // NavigationCompleted is triggered for every Load. If an application uses reloads the Hide/Show will trigger