feat(webkit): implement popup window signal handling and related webview creation
Implements popup window support in WebKit adapter: - Added PopupRequest type for popup metadata (target URI, frame name, user gesture, parent ID) - Implemented NewWebViewWithRelated constructor for creating popups that share session/cookies with parent - Connected 'create' signal to OnCreate callback for popup creation/blocking - Connected 'ready-to-show' signal to OnReadyToShow callback for popup display lifecycle - Added gtk import for Widget type in signal handling
- commit
- 27707ff0d67e838757b048353c584a3796a5ea54
- author
- bnema · · 2025-12-09 22:58 UTC
- parent
- cc823ac
- signature
- unsigned
1 file · +91 -2
M internal/infrastructure/webkit/webview.go +91 -2
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "sync/atomic" | |
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/puregotk-webkit/webkit" | |
| 9 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 9 | 10 | "github.com/rs/zerolog" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| @@ -22,6 +23,14 @@ const ( | |||
| 22 | 23 | LoadFinished LoadEvent = LoadEvent(webkit.LoadFinishedValue) | |
| 23 | 24 | ) | |
| 24 | 25 | ||
| 26 | + | // PopupRequest contains information about a popup window request from the create signal. | |
| 27 | + | type PopupRequest struct { | |
| 28 | + | TargetURI string | |
| 29 | + | FrameName string // e.g., "_blank", custom name, or empty | |
| 30 | + | IsUserGesture bool | |
| 31 | + | ParentID WebViewID | |
| 32 | + | } | |
| 33 | + | ||
| 25 | 34 | // webViewRegistry tracks all active WebViews. | |
| 26 | 35 | type webViewRegistry struct { | |
| 27 | 36 | views map[WebViewID]*WebView | |
| @@ -82,8 +91,8 @@ type WebView struct { | |||
| 82 | 91 | OnURIChanged func(string) | |
| 83 | 92 | OnProgressChanged func(float64) | |
| 84 | 93 | OnClose func() | |
| 85 | - | // TODO(puregotk-webkit): OnCreate for popup support | |
| 86 | - | // See: /home/brice/sync/alpha/obsidian/projects/puregotk-webkit/issues/related-webview.md | |
| 94 | + | OnCreate func(PopupRequest) *WebView // Return new WebView or nil to block popup | |
| 95 | + | OnReadyToShow func() // Called when popup is ready to display | |
| 87 | 96 | ||
| 88 | 97 | logger zerolog.Logger | |
| 89 | 98 | mu sync.RWMutex | |
| @@ -122,6 +131,43 @@ func NewWebView(wkCtx *WebKitContext, settings *SettingsManager, logger zerolog. | |||
| 122 | 131 | return wv, nil | |
| 123 | 132 | } | |
| 124 | 133 | ||
| 134 | + | // NewWebViewWithRelated creates a WebView that shares session/cookies with parent. | |
| 135 | + | // This is required for popup windows to maintain authentication state. | |
| 136 | + | func NewWebViewWithRelated(parent *WebView, settings *SettingsManager, logger zerolog.Logger) (*WebView, error) { | |
| 137 | + | if parent == nil { | |
| 138 | + | return nil, fmt.Errorf("parent webview is nil") | |
| 139 | + | } | |
| 140 | + | if parent.IsDestroyed() { | |
| 141 | + | return nil, fmt.Errorf("parent webview %d is destroyed", parent.id) | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | inner := webkit.NewWebViewWithRelatedView(parent.inner) | |
| 145 | + | if inner == nil { | |
| 146 | + | return nil, fmt.Errorf("failed to create related webkit webview") | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | wv := &WebView{ | |
| 150 | + | inner: inner, | |
| 151 | + | logger: logger.With().Str("component", "webview-popup").Logger(), | |
| 152 | + | signalIDs: make([]uint32, 0, 6), | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | wv.id = globalRegistry.register(wv) | |
| 156 | + | ||
| 157 | + | if settings != nil { | |
| 158 | + | settings.ApplyToWebView(inner) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | wv.connectSignals() | |
| 162 | + | ||
| 163 | + | wv.logger.Debug(). | |
| 164 | + | Uint64("id", uint64(wv.id)). | |
| 165 | + | Uint64("parent_id", uint64(parent.id)). | |
| 166 | + | Msg("related webview created for popup") | |
| 167 | + | ||
| 168 | + | return wv, nil | |
| 169 | + | } | |
| 170 | + | ||
| 125 | 171 | // connectSignals sets up signal handlers for the WebView. | |
| 126 | 172 | func (wv *WebView) connectSignals() { | |
| 127 | 173 | // load-changed signal | |
| @@ -157,6 +203,49 @@ func (wv *WebView) connectSignals() { | |||
| 157 | 203 | sigID = wv.inner.ConnectClose(&closeCb) | |
| 158 | 204 | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 159 | 205 | ||
| 206 | + | // create signal for popup window handling | |
| 207 | + | createCb := func(inner webkit.WebView, navActionPtr uintptr) gtk.Widget { | |
| 208 | + | if wv.OnCreate == nil { | |
| 209 | + | return gtk.Widget{} // Block popup | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | navAction := webkit.NavigationActionFromPointer(navActionPtr) | |
| 213 | + | if navAction == nil { | |
| 214 | + | return gtk.Widget{} // Block popup | |
| 215 | + | } | |
| 216 | + | defer navAction.Free() | |
| 217 | + | ||
| 218 | + | var targetURI string | |
| 219 | + | if req := navAction.GetRequest(); req != nil { | |
| 220 | + | targetURI = req.GetUri() | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | popupReq := PopupRequest{ | |
| 224 | + | TargetURI: targetURI, | |
| 225 | + | FrameName: navAction.GetFrameName(), | |
| 226 | + | IsUserGesture: navAction.IsUserGesture(), | |
| 227 | + | ParentID: wv.id, | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | newWV := wv.OnCreate(popupReq) | |
| 231 | + | if newWV == nil { | |
| 232 | + | return gtk.Widget{} // Block popup | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | return newWV.inner.Widget | |
| 236 | + | } | |
| 237 | + | sigID = wv.inner.ConnectCreate(&createCb) | |
| 238 | + | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 239 | + | ||
| 240 | + | // ready-to-show signal for popup display | |
| 241 | + | readyToShowCb := func(inner webkit.WebView) { | |
| 242 | + | if wv.OnReadyToShow != nil { | |
| 243 | + | wv.OnReadyToShow() | |
| 244 | + | } | |
| 245 | + | } | |
| 246 | + | sigID = wv.inner.ConnectReadyToShow(&readyToShowCb) | |
| 247 | + | wv.signalIDs = append(wv.signalIDs, sigID) | |
| 248 | + | ||
| 160 | 249 | // Note: notify::title, notify::uri, notify::estimated-load-progress | |
| 161 | 250 | // would need GObject property change notifications which may require | |
| 162 | 251 | // different API patterns in puregotk. For now, we update these in | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "sync/atomic" | 6 | "sync/atomic" | ||
| 7 | 7 | ||||
| 8 | "github.com/bnema/puregotk-webkit/webkit" | 8 | "github.com/bnema/puregotk-webkit/webkit" | ||
| 9 | + | "github.com/jwijenbergh/puregotk/v4/gtk" | |||
| 9 | "github.com/rs/zerolog" | 10 | "github.com/rs/zerolog" | ||
| 10 | ) | 11 | ) | ||
| 11 | 12 | ||||
| @@ -22,6 +23,14 @@ const ( | |||||
| 22 | LoadFinished LoadEvent = LoadEvent(webkit.LoadFinishedValue) | 23 | LoadFinished LoadEvent = LoadEvent(webkit.LoadFinishedValue) | ||
| 23 | ) | 24 | ) | ||
| 24 | 25 | ||||
| 26 | + | // PopupRequest contains information about a popup window request from the create signal. | |||
| 27 | + | type PopupRequest struct { | |||
| 28 | + | TargetURI string | |||
| 29 | + | FrameName string // e.g., "_blank", custom name, or empty | |||
| 30 | + | IsUserGesture bool | |||
| 31 | + | ParentID WebViewID | |||
| 32 | + | } | |||
| 33 | + | ||||
| 25 | // webViewRegistry tracks all active WebViews. | 34 | // webViewRegistry tracks all active WebViews. | ||
| 26 | type webViewRegistry struct { | 35 | type webViewRegistry struct { | ||
| 27 | views map[WebViewID]*WebView | 36 | views map[WebViewID]*WebView | ||
| @@ -82,8 +91,8 @@ type WebView struct { | |||||
| 82 | OnURIChanged func(string) | 91 | OnURIChanged func(string) | ||
| 83 | OnProgressChanged func(float64) | 92 | OnProgressChanged func(float64) | ||
| 84 | OnClose func() | 93 | OnClose func() | ||
| 85 | - | // TODO(puregotk-webkit): OnCreate for popup support | 94 | + | OnCreate func(PopupRequest) *WebView // Return new WebView or nil to block popup |
| 86 | - | // See: /home/brice/sync/alpha/obsidian/projects/puregotk-webkit/issues/related-webview.md | 95 | + | OnReadyToShow func() // Called when popup is ready to display |
| 87 | 96 | ||||
| 88 | logger zerolog.Logger | 97 | logger zerolog.Logger | ||
| 89 | mu sync.RWMutex | 98 | mu sync.RWMutex | ||
| @@ -122,6 +131,43 @@ func NewWebView(wkCtx *WebKitContext, settings *SettingsManager, logger zerolog. | |||||
| 122 | return wv, nil | 131 | return wv, nil | ||
| 123 | } | 132 | } | ||
| 124 | 133 | ||||
| 134 | + | // NewWebViewWithRelated creates a WebView that shares session/cookies with parent. | |||
| 135 | + | // This is required for popup windows to maintain authentication state. | |||
| 136 | + | func NewWebViewWithRelated(parent *WebView, settings *SettingsManager, logger zerolog.Logger) (*WebView, error) { | |||
| 137 | + | if parent == nil { | |||
| 138 | + | return nil, fmt.Errorf("parent webview is nil") | |||
| 139 | + | } | |||
| 140 | + | if parent.IsDestroyed() { | |||
| 141 | + | return nil, fmt.Errorf("parent webview %d is destroyed", parent.id) | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | inner := webkit.NewWebViewWithRelatedView(parent.inner) | |||
| 145 | + | if inner == nil { | |||
| 146 | + | return nil, fmt.Errorf("failed to create related webkit webview") | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | wv := &WebView{ | |||
| 150 | + | inner: inner, | |||
| 151 | + | logger: logger.With().Str("component", "webview-popup").Logger(), | |||
| 152 | + | signalIDs: make([]uint32, 0, 6), | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | wv.id = globalRegistry.register(wv) | |||
| 156 | + | ||||
| 157 | + | if settings != nil { | |||
| 158 | + | settings.ApplyToWebView(inner) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | wv.connectSignals() | |||
| 162 | + | ||||
| 163 | + | wv.logger.Debug(). | |||
| 164 | + | Uint64("id", uint64(wv.id)). | |||
| 165 | + | Uint64("parent_id", uint64(parent.id)). | |||
| 166 | + | Msg("related webview created for popup") | |||
| 167 | + | ||||
| 168 | + | return wv, nil | |||
| 169 | + | } | |||
| 170 | + | ||||
| 125 | // connectSignals sets up signal handlers for the WebView. | 171 | // connectSignals sets up signal handlers for the WebView. | ||
| 126 | func (wv *WebView) connectSignals() { | 172 | func (wv *WebView) connectSignals() { | ||
| 127 | // load-changed signal | 173 | // load-changed signal | ||
| @@ -157,6 +203,49 @@ func (wv *WebView) connectSignals() { | |||||
| 157 | sigID = wv.inner.ConnectClose(&closeCb) | 203 | sigID = wv.inner.ConnectClose(&closeCb) | ||
| 158 | wv.signalIDs = append(wv.signalIDs, sigID) | 204 | wv.signalIDs = append(wv.signalIDs, sigID) | ||
| 159 | 205 | ||||
| 206 | + | // create signal for popup window handling | |||
| 207 | + | createCb := func(inner webkit.WebView, navActionPtr uintptr) gtk.Widget { | |||
| 208 | + | if wv.OnCreate == nil { | |||
| 209 | + | return gtk.Widget{} // Block popup | |||
| 210 | + | } | |||
| 211 | + | ||||
| 212 | + | navAction := webkit.NavigationActionFromPointer(navActionPtr) | |||
| 213 | + | if navAction == nil { | |||
| 214 | + | return gtk.Widget{} // Block popup | |||
| 215 | + | } | |||
| 216 | + | defer navAction.Free() | |||
| 217 | + | ||||
| 218 | + | var targetURI string | |||
| 219 | + | if req := navAction.GetRequest(); req != nil { | |||
| 220 | + | targetURI = req.GetUri() | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | popupReq := PopupRequest{ | |||
| 224 | + | TargetURI: targetURI, | |||
| 225 | + | FrameName: navAction.GetFrameName(), | |||
| 226 | + | IsUserGesture: navAction.IsUserGesture(), | |||
| 227 | + | ParentID: wv.id, | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | newWV := wv.OnCreate(popupReq) | |||
| 231 | + | if newWV == nil { | |||
| 232 | + | return gtk.Widget{} // Block popup | |||
| 233 | + | } | |||
| 234 | + | ||||
| 235 | + | return newWV.inner.Widget | |||
| 236 | + | } | |||
| 237 | + | sigID = wv.inner.ConnectCreate(&createCb) | |||
| 238 | + | wv.signalIDs = append(wv.signalIDs, sigID) | |||
| 239 | + | ||||
| 240 | + | // ready-to-show signal for popup display | |||
| 241 | + | readyToShowCb := func(inner webkit.WebView) { | |||
| 242 | + | if wv.OnReadyToShow != nil { | |||
| 243 | + | wv.OnReadyToShow() | |||
| 244 | + | } | |||
| 245 | + | } | |||
| 246 | + | sigID = wv.inner.ConnectReadyToShow(&readyToShowCb) | |||
| 247 | + | wv.signalIDs = append(wv.signalIDs, sigID) | |||
| 248 | + | ||||
| 160 | // Note: notify::title, notify::uri, notify::estimated-load-progress | 249 | // Note: notify::title, notify::uri, notify::estimated-load-progress | ||
| 161 | // would need GObject property change notifications which may require | 250 | // would need GObject property change notifications which may require | ||
| 162 | // different API patterns in puregotk. For now, we update these in | 251 | // different API patterns in puregotk. For now, we update these in | ||