feat(webkit): implement popup handler with webkit create signal
- commit
- d9972d972d3525e59823783407a37f2062713dff
- author
- bamen · · 2025-09-20 05:21 UTC
- parent
- d0eaf06
- signature
- unsigned
4 files · +345 -62
M internal/app/browser/webview.go +9 -24
| @@ -186,35 +186,20 @@ func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) { | |||
| 186 | 186 | pane.messageHandler.SetNavigationController(pane.navigationController) | |
| 187 | 187 | } | |
| 188 | 188 | ||
| 189 | - | pane.webView.RegisterPopupHandler(func(uri string) bool { | |
| 190 | - | handled := false | |
| 189 | + | pane.webView.RegisterPopupHandler(func(uri string) *webkit.WebView { | |
| 191 | 190 | if app.workspace != nil { | |
| 192 | - | handled = app.workspace.HandlePopup(pane.webView, uri) | |
| 193 | - | } | |
| 194 | - | if handled { | |
| 195 | - | return true | |
| 191 | + | return app.workspace.HandlePopup(pane.webView, uri) | |
| 196 | 192 | } | |
| 197 | 193 | ||
| 198 | - | if pane.navigationController != nil && uri != "" { | |
| 199 | - | if err := pane.navigationController.NavigateToURL(uri); err != nil { | |
| 200 | - | log.Printf("[workspace] popup fallback navigation failed: %v", err) | |
| 201 | - | if pane.webView != nil { | |
| 202 | - | if loadErr := pane.webView.LoadURL(uri); loadErr != nil { | |
| 203 | - | log.Printf("[workspace] popup fallback load failed: %v", loadErr) | |
| 204 | - | } | |
| 194 | + | // No workspace manager - just navigate in current pane | |
| 195 | + | if uri != "" && pane.navigationController != nil { | |
| 196 | + | go func() { | |
| 197 | + | if err := pane.navigationController.NavigateToURL(uri); err != nil { | |
| 198 | + | log.Printf("[webview] failed to navigate to URL %s: %v", uri, err) | |
| 205 | 199 | } | |
| 206 | - | } | |
| 207 | - | return true | |
| 208 | - | } | |
| 209 | - | ||
| 210 | - | if pane.webView != nil && uri != "" { | |
| 211 | - | if err := pane.webView.LoadURL(uri); err != nil { | |
| 212 | - | log.Printf("[workspace] popup direct load failed: %v", err) | |
| 213 | - | } | |
| 214 | - | return true | |
| 200 | + | }() | |
| 215 | 201 | } | |
| 216 | - | ||
| 217 | - | return false | |
| 202 | + | return nil | |
| 218 | 203 | }) | |
| 219 | 204 | } | |
| 220 | 205 | ||
| @@ -186,35 +186,20 @@ func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) { | |||||
| 186 | pane.messageHandler.SetNavigationController(pane.navigationController) | 186 | pane.messageHandler.SetNavigationController(pane.navigationController) | ||
| 187 | } | 187 | } | ||
| 188 | 188 | ||||
| 189 | - | pane.webView.RegisterPopupHandler(func(uri string) bool { | 189 | + | pane.webView.RegisterPopupHandler(func(uri string) *webkit.WebView { |
| 190 | - | handled := false | |||
| 191 | if app.workspace != nil { | 190 | if app.workspace != nil { | ||
| 192 | - | handled = app.workspace.HandlePopup(pane.webView, uri) | 191 | + | return app.workspace.HandlePopup(pane.webView, uri) |
| 193 | - | } | |||
| 194 | - | if handled { | |||
| 195 | - | return true | |||
| 196 | } | 192 | } | ||
| 197 | 193 | ||||
| 198 | - | if pane.navigationController != nil && uri != "" { | 194 | + | // No workspace manager - just navigate in current pane |
| 199 | - | if err := pane.navigationController.NavigateToURL(uri); err != nil { | 195 | + | if uri != "" && pane.navigationController != nil { |
| 200 | - | log.Printf("[workspace] popup fallback navigation failed: %v", err) | 196 | + | go func() { |
| 201 | - | if pane.webView != nil { | 197 | + | if err := pane.navigationController.NavigateToURL(uri); err != nil { |
| 202 | - | if loadErr := pane.webView.LoadURL(uri); loadErr != nil { | 198 | + | log.Printf("[webview] failed to navigate to URL %s: %v", uri, err) |
| 203 | - | log.Printf("[workspace] popup fallback load failed: %v", loadErr) | |||
| 204 | - | } | |||
| 205 | } | 199 | } | ||
| 206 | - | } | 200 | + | }() |
| 207 | - | return true | |||
| 208 | - | } | |||
| 209 | - | ||||
| 210 | - | if pane.webView != nil && uri != "" { | |||
| 211 | - | if err := pane.webView.LoadURL(uri); err != nil { | |||
| 212 | - | log.Printf("[workspace] popup direct load failed: %v", err) | |||
| 213 | - | } | |||
| 214 | - | return true | |||
| 215 | } | 201 | } | ||
| 216 | - | 202 | + | return nil | |
| 217 | - | return false | |||
| 218 | }) | 203 | }) | ||
| 219 | } | 204 | } | ||
| 220 | 205 | ||||
M pkg/webkit/keyboard_cgo.go +95 -7
| @@ -84,6 +84,7 @@ import ( | |||
| 84 | 84 | "sync" | |
| 85 | 85 | "sync/atomic" | |
| 86 | 86 | "time" | |
| 87 | + | "unsafe" | |
| 87 | 88 | ) | |
| 88 | 89 | ||
| 89 | 90 | // Registry of accelerators per view id. | |
| @@ -436,23 +437,110 @@ func goOnURIChanged(id C.ulong, curi *C.char) { | |||
| 436 | 437 | vw.dispatchURIChanged(uri) | |
| 437 | 438 | } | |
| 438 | 439 | ||
| 439 | - | //export goOnPopupRequest | |
| 440 | - | func goOnPopupRequest(id C.ulong, curi *C.char) C.gboolean { | |
| 440 | + | //export goHandleNewWindowPolicy | |
| 441 | + | func goHandleNewWindowPolicy(id C.ulong, curi *C.char, navType C.int) C.gboolean { | |
| 441 | 442 | uid := uintptr(id) | |
| 442 | 443 | regMu.RLock() | |
| 443 | 444 | vw := viewByID[uid] | |
| 444 | 445 | regMu.RUnlock() | |
| 446 | + | ||
| 445 | 447 | if vw == nil { | |
| 446 | - | return C.gboolean(0) | |
| 448 | + | log.Printf("[webkit] Policy decision: no WebView found for ID %d", uid) | |
| 449 | + | return 0 // FALSE | |
| 447 | 450 | } | |
| 448 | - | var uri string | |
| 451 | + | ||
| 452 | + | uri := "" | |
| 449 | 453 | if curi != nil { | |
| 450 | 454 | uri = C.GoString(curi) | |
| 451 | 455 | } | |
| 452 | - | if vw.dispatchPopupRequest(uri) { | |
| 453 | - | return C.gboolean(1) | |
| 456 | + | ||
| 457 | + | // Navigation type constants from WebKit headers | |
| 458 | + | navTypeNames := map[int]string{ | |
| 459 | + | 0: "LINK_CLICKED", | |
| 460 | + | 1: "FORM_SUBMITTED", | |
| 461 | + | 2: "BACK_FORWARD", | |
| 462 | + | 3: "RELOAD", | |
| 463 | + | 4: "FORM_RESUBMITTED", | |
| 464 | + | 5: "OTHER", | |
| 454 | 465 | } | |
| 455 | - | return C.gboolean(0) | |
| 466 | + | ||
| 467 | + | navTypeName := navTypeNames[int(navType)] | |
| 468 | + | if navTypeName == "" { | |
| 469 | + | navTypeName = fmt.Sprintf("UNKNOWN(%d)", int(navType)) | |
| 470 | + | } | |
| 471 | + | ||
| 472 | + | log.Printf("[webkit] Policy decision for new window: URI=%s, NavType=%s", uri, navTypeName) | |
| 473 | + | ||
| 474 | + | // Check if we have a popup handler and if popups are enabled | |
| 475 | + | if vw.popupHandler != nil { | |
| 476 | + | log.Printf("[webkit] Calling popup handler for URI: %s", uri) | |
| 477 | + | // Call popup handler synchronously to get immediate result | |
| 478 | + | result := vw.popupHandler(uri) | |
| 479 | + | if result != nil { | |
| 480 | + | log.Printf("[webkit] Popup handler created new WebView for: %s - blocking default popup", uri) | |
| 481 | + | return 1 // TRUE - we handled it, block the default popup | |
| 482 | + | } else { | |
| 483 | + | log.Printf("[webkit] Popup handler declined to create WebView for: %s - allowing native popup", uri) | |
| 484 | + | return 0 // FALSE - we didn't handle it, allow default popup | |
| 485 | + | } | |
| 486 | + | } else { | |
| 487 | + | log.Printf("[webkit] No popup handler registered - allowing native popup for: %s", uri) | |
| 488 | + | return 0 // FALSE - no handler, allow default popup | |
| 489 | + | } | |
| 490 | + | } | |
| 491 | + | ||
| 492 | + | //export goHandleCreateWebView | |
| 493 | + | func goHandleCreateWebView(id C.ulong, curi *C.char) *C.GtkWidget { | |
| 494 | + | uid := uintptr(id) | |
| 495 | + | regMu.RLock() | |
| 496 | + | vw := viewByID[uid] | |
| 497 | + | regMu.RUnlock() | |
| 498 | + | ||
| 499 | + | if vw == nil { | |
| 500 | + | log.Printf("[webkit] Create signal: no WebView found for ID %d", uid) | |
| 501 | + | return nil // Fallback to native popup | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | uri := "" | |
| 505 | + | if curi != nil { | |
| 506 | + | uri = C.GoString(curi) | |
| 507 | + | } | |
| 508 | + | ||
| 509 | + | log.Printf("[webkit] Create signal: creating new pane for URI=%s", uri) | |
| 510 | + | ||
| 511 | + | // Check if we have a popup handler | |
| 512 | + | if vw.popupHandler == nil { | |
| 513 | + | log.Printf("[webkit] Create signal: no popup handler registered") | |
| 514 | + | return nil // Fallback to native popup | |
| 515 | + | } | |
| 516 | + | ||
| 517 | + | // Always use the popup handler to create workspace pane (tiling WM design) | |
| 518 | + | newView := vw.popupHandler(uri) | |
| 519 | + | if newView != nil { | |
| 520 | + | widget := newView.Widget() | |
| 521 | + | if widget != 0 { | |
| 522 | + | log.Printf("[webkit] Create signal: created new workspace pane for popup") | |
| 523 | + | return (*C.GtkWidget)(unsafe.Pointer(widget)) | |
| 524 | + | } | |
| 525 | + | } | |
| 526 | + | ||
| 527 | + | log.Printf("[webkit] Create signal: popup handler failed to create pane, falling back to native") | |
| 528 | + | return nil // Fallback to native popup if workspace creation fails | |
| 529 | + | } | |
| 530 | + | ||
| 531 | + | ||
| 532 | + | //export goHandlePopupGeometry | |
| 533 | + | func goHandlePopupGeometry(id C.ulong, x, y, width, height C.int) { | |
| 534 | + | // Store geometry for workspace manager to use | |
| 535 | + | uid := uintptr(id) | |
| 536 | + | regMu.Lock() | |
| 537 | + | if vw := viewByID[uid]; vw != nil { | |
| 538 | + | // For now, we'll store this in a simple way | |
| 539 | + | // The workspace manager will handle the geometry | |
| 540 | + | log.Printf("[webkit] Popup geometry: %dx%d at (%d,%d)", | |
| 541 | + | int(width), int(height), int(x), int(y)) | |
| 542 | + | } | |
| 543 | + | regMu.Unlock() | |
| 456 | 544 | } | |
| 457 | 545 | ||
| 458 | 546 | //export goOnButtonPress | |
| @@ -84,6 +84,7 @@ import ( | |||||
| 84 | "sync" | 84 | "sync" | ||
| 85 | "sync/atomic" | 85 | "sync/atomic" | ||
| 86 | "time" | 86 | "time" | ||
| 87 | + | "unsafe" | |||
| 87 | ) | 88 | ) | ||
| 88 | 89 | ||||
| 89 | // Registry of accelerators per view id. | 90 | // Registry of accelerators per view id. | ||
| @@ -436,23 +437,110 @@ func goOnURIChanged(id C.ulong, curi *C.char) { | |||||
| 436 | vw.dispatchURIChanged(uri) | 437 | vw.dispatchURIChanged(uri) | ||
| 437 | } | 438 | } | ||
| 438 | 439 | ||||
| 439 | - | //export goOnPopupRequest | 440 | + | //export goHandleNewWindowPolicy |
| 440 | - | func goOnPopupRequest(id C.ulong, curi *C.char) C.gboolean { | 441 | + | func goHandleNewWindowPolicy(id C.ulong, curi *C.char, navType C.int) C.gboolean { |
| 441 | uid := uintptr(id) | 442 | uid := uintptr(id) | ||
| 442 | regMu.RLock() | 443 | regMu.RLock() | ||
| 443 | vw := viewByID[uid] | 444 | vw := viewByID[uid] | ||
| 444 | regMu.RUnlock() | 445 | regMu.RUnlock() | ||
| 446 | + | ||||
| 445 | if vw == nil { | 447 | if vw == nil { | ||
| 446 | - | return C.gboolean(0) | 448 | + | log.Printf("[webkit] Policy decision: no WebView found for ID %d", uid) |
| 449 | + | return 0 // FALSE | |||
| 447 | } | 450 | } | ||
| 448 | - | var uri string | 451 | + | |
| 452 | + | uri := "" | |||
| 449 | if curi != nil { | 453 | if curi != nil { | ||
| 450 | uri = C.GoString(curi) | 454 | uri = C.GoString(curi) | ||
| 451 | } | 455 | } | ||
| 452 | - | if vw.dispatchPopupRequest(uri) { | 456 | + | |
| 453 | - | return C.gboolean(1) | 457 | + | // Navigation type constants from WebKit headers |
| 458 | + | navTypeNames := map[int]string{ | |||
| 459 | + | 0: "LINK_CLICKED", | |||
| 460 | + | 1: "FORM_SUBMITTED", | |||
| 461 | + | 2: "BACK_FORWARD", | |||
| 462 | + | 3: "RELOAD", | |||
| 463 | + | 4: "FORM_RESUBMITTED", | |||
| 464 | + | 5: "OTHER", | |||
| 454 | } | 465 | } | ||
| 455 | - | return C.gboolean(0) | 466 | + | |
| 467 | + | navTypeName := navTypeNames[int(navType)] | |||
| 468 | + | if navTypeName == "" { | |||
| 469 | + | navTypeName = fmt.Sprintf("UNKNOWN(%d)", int(navType)) | |||
| 470 | + | } | |||
| 471 | + | ||||
| 472 | + | log.Printf("[webkit] Policy decision for new window: URI=%s, NavType=%s", uri, navTypeName) | |||
| 473 | + | ||||
| 474 | + | // Check if we have a popup handler and if popups are enabled | |||
| 475 | + | if vw.popupHandler != nil { | |||
| 476 | + | log.Printf("[webkit] Calling popup handler for URI: %s", uri) | |||
| 477 | + | // Call popup handler synchronously to get immediate result | |||
| 478 | + | result := vw.popupHandler(uri) | |||
| 479 | + | if result != nil { | |||
| 480 | + | log.Printf("[webkit] Popup handler created new WebView for: %s - blocking default popup", uri) | |||
| 481 | + | return 1 // TRUE - we handled it, block the default popup | |||
| 482 | + | } else { | |||
| 483 | + | log.Printf("[webkit] Popup handler declined to create WebView for: %s - allowing native popup", uri) | |||
| 484 | + | return 0 // FALSE - we didn't handle it, allow default popup | |||
| 485 | + | } | |||
| 486 | + | } else { | |||
| 487 | + | log.Printf("[webkit] No popup handler registered - allowing native popup for: %s", uri) | |||
| 488 | + | return 0 // FALSE - no handler, allow default popup | |||
| 489 | + | } | |||
| 490 | + | } | |||
| 491 | + | ||||
| 492 | + | //export goHandleCreateWebView | |||
| 493 | + | func goHandleCreateWebView(id C.ulong, curi *C.char) *C.GtkWidget { | |||
| 494 | + | uid := uintptr(id) | |||
| 495 | + | regMu.RLock() | |||
| 496 | + | vw := viewByID[uid] | |||
| 497 | + | regMu.RUnlock() | |||
| 498 | + | ||||
| 499 | + | if vw == nil { | |||
| 500 | + | log.Printf("[webkit] Create signal: no WebView found for ID %d", uid) | |||
| 501 | + | return nil // Fallback to native popup | |||
| 502 | + | } | |||
| 503 | + | ||||
| 504 | + | uri := "" | |||
| 505 | + | if curi != nil { | |||
| 506 | + | uri = C.GoString(curi) | |||
| 507 | + | } | |||
| 508 | + | ||||
| 509 | + | log.Printf("[webkit] Create signal: creating new pane for URI=%s", uri) | |||
| 510 | + | ||||
| 511 | + | // Check if we have a popup handler | |||
| 512 | + | if vw.popupHandler == nil { | |||
| 513 | + | log.Printf("[webkit] Create signal: no popup handler registered") | |||
| 514 | + | return nil // Fallback to native popup | |||
| 515 | + | } | |||
| 516 | + | ||||
| 517 | + | // Always use the popup handler to create workspace pane (tiling WM design) | |||
| 518 | + | newView := vw.popupHandler(uri) | |||
| 519 | + | if newView != nil { | |||
| 520 | + | widget := newView.Widget() | |||
| 521 | + | if widget != 0 { | |||
| 522 | + | log.Printf("[webkit] Create signal: created new workspace pane for popup") | |||
| 523 | + | return (*C.GtkWidget)(unsafe.Pointer(widget)) | |||
| 524 | + | } | |||
| 525 | + | } | |||
| 526 | + | ||||
| 527 | + | log.Printf("[webkit] Create signal: popup handler failed to create pane, falling back to native") | |||
| 528 | + | return nil // Fallback to native popup if workspace creation fails | |||
| 529 | + | } | |||
| 530 | + | ||||
| 531 | + | ||||
| 532 | + | //export goHandlePopupGeometry | |||
| 533 | + | func goHandlePopupGeometry(id C.ulong, x, y, width, height C.int) { | |||
| 534 | + | // Store geometry for workspace manager to use | |||
| 535 | + | uid := uintptr(id) | |||
| 536 | + | regMu.Lock() | |||
| 537 | + | if vw := viewByID[uid]; vw != nil { | |||
| 538 | + | // For now, we'll store this in a simple way | |||
| 539 | + | // The workspace manager will handle the geometry | |||
| 540 | + | log.Printf("[webkit] Popup geometry: %dx%d at (%d,%d)", | |||
| 541 | + | int(width), int(height), int(x), int(y)) | |||
| 542 | + | } | |||
| 543 | + | regMu.Unlock() | |||
| 456 | } | 544 | } | ||
| 457 | 545 | ||||
| 458 | //export goOnButtonPress | 546 | //export goOnButtonPress | ||
M pkg/webkit/webview.go +16 -4
| @@ -5,6 +5,7 @@ package webkit | |||
| 5 | 5 | import ( | |
| 6 | 6 | "fmt" | |
| 7 | 7 | "log" | |
| 8 | + | "unsafe" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| 10 | 11 | // WebView represents a browser view powered by WebKit2GTK. | |
| @@ -20,7 +21,7 @@ type WebView struct { | |||
| 20 | 21 | titleHandler func(title string) | |
| 21 | 22 | uriHandler func(uri string) | |
| 22 | 23 | zoomHandler func(level float64) | |
| 23 | - | popupHandler func(string) bool | |
| 24 | + | popupHandler func(string) *WebView | |
| 24 | 25 | useDomZoom bool | |
| 25 | 26 | domZoomSeed float64 | |
| 26 | 27 | container uintptr | |
| @@ -45,6 +46,12 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 45 | 46 | return wv, nil | |
| 46 | 47 | } | |
| 47 | 48 | ||
| 49 | + | // NewWebViewWithRelated creates a new WebView related to an existing one (non-CGO stub) | |
| 50 | + | func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) { | |
| 51 | + | // In non-CGO build, just create a regular WebView | |
| 52 | + | return NewWebView(cfg) | |
| 53 | + | } | |
| 54 | + | ||
| 48 | 55 | // LoadURL navigates the webview to the specified URL. | |
| 49 | 56 | func (w *WebView) LoadURL(rawURL string) error { | |
| 50 | 57 | if w == nil || w.destroyed { | |
| @@ -114,7 +121,7 @@ func (w *WebView) CloseDevTools() error { return nil } | |||
| 114 | 121 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | |
| 115 | 122 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | |
| 116 | 123 | ||
| 117 | - | func (w *WebView) RegisterPopupHandler(cb func(string) bool) { w.popupHandler = cb } | |
| 124 | + | func (w *WebView) RegisterPopupHandler(cb func(string) *WebView) { w.popupHandler = cb } | |
| 118 | 125 | ||
| 119 | 126 | func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Called from CGO WebKit callbacks | |
| 120 | 127 | if w != nil && w.msgHandler != nil { | |
| @@ -122,11 +129,16 @@ func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Cal | |||
| 122 | 129 | } | |
| 123 | 130 | } | |
| 124 | 131 | ||
| 125 | - | func (w *WebView) dispatchPopupRequest(uri string) bool { //nolint:unused // Called from CGO WebKit callbacks | |
| 132 | + | func (w *WebView) dispatchPopupRequest(uri string) *WebView { //nolint:unused // Called from CGO WebKit callbacks | |
| 126 | 133 | if w != nil && w.popupHandler != nil { | |
| 127 | 134 | return w.popupHandler(uri) | |
| 128 | 135 | } | |
| 129 | - | return false | |
| 136 | + | return nil | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | // GetNativePointer returns nil for non-CGO build (stub implementation) | |
| 140 | + | func (w *WebView) GetNativePointer() unsafe.Pointer { //nolint:unused // Needed for interface compatibility | |
| 141 | + | return nil | |
| 130 | 142 | } | |
| 131 | 143 | ||
| 132 | 144 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | |
| @@ -5,6 +5,7 @@ package webkit | |||||
| 5 | import ( | 5 | import ( | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "log" | 7 | "log" | ||
| 8 | + | "unsafe" | |||
| 8 | ) | 9 | ) | ||
| 9 | 10 | ||||
| 10 | // WebView represents a browser view powered by WebKit2GTK. | 11 | // WebView represents a browser view powered by WebKit2GTK. | ||
| @@ -20,7 +21,7 @@ type WebView struct { | |||||
| 20 | titleHandler func(title string) | 21 | titleHandler func(title string) | ||
| 21 | uriHandler func(uri string) | 22 | uriHandler func(uri string) | ||
| 22 | zoomHandler func(level float64) | 23 | zoomHandler func(level float64) | ||
| 23 | - | popupHandler func(string) bool | 24 | + | popupHandler func(string) *WebView |
| 24 | useDomZoom bool | 25 | useDomZoom bool | ||
| 25 | domZoomSeed float64 | 26 | domZoomSeed float64 | ||
| 26 | container uintptr | 27 | container uintptr | ||
| @@ -45,6 +46,12 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 45 | return wv, nil | 46 | return wv, nil | ||
| 46 | } | 47 | } | ||
| 47 | 48 | ||||
| 49 | + | // NewWebViewWithRelated creates a new WebView related to an existing one (non-CGO stub) | |||
| 50 | + | func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) { | |||
| 51 | + | // In non-CGO build, just create a regular WebView | |||
| 52 | + | return NewWebView(cfg) | |||
| 53 | + | } | |||
| 54 | + | ||||
| 48 | // LoadURL navigates the webview to the specified URL. | 55 | // LoadURL navigates the webview to the specified URL. | ||
| 49 | func (w *WebView) LoadURL(rawURL string) error { | 56 | func (w *WebView) LoadURL(rawURL string) error { | ||
| 50 | if w == nil || w.destroyed { | 57 | if w == nil || w.destroyed { | ||
| @@ -114,7 +121,7 @@ func (w *WebView) CloseDevTools() error { return nil } | |||||
| 114 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | 121 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | ||
| 115 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | 122 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | ||
| 116 | 123 | ||||
| 117 | - | func (w *WebView) RegisterPopupHandler(cb func(string) bool) { w.popupHandler = cb } | 124 | + | func (w *WebView) RegisterPopupHandler(cb func(string) *WebView) { w.popupHandler = cb } |
| 118 | 125 | ||||
| 119 | func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Called from CGO WebKit callbacks | 126 | func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Called from CGO WebKit callbacks | ||
| 120 | if w != nil && w.msgHandler != nil { | 127 | if w != nil && w.msgHandler != nil { | ||
| @@ -122,11 +129,16 @@ func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Cal | |||||
| 122 | } | 129 | } | ||
| 123 | } | 130 | } | ||
| 124 | 131 | ||||
| 125 | - | func (w *WebView) dispatchPopupRequest(uri string) bool { //nolint:unused // Called from CGO WebKit callbacks | 132 | + | func (w *WebView) dispatchPopupRequest(uri string) *WebView { //nolint:unused // Called from CGO WebKit callbacks |
| 126 | if w != nil && w.popupHandler != nil { | 133 | if w != nil && w.popupHandler != nil { | ||
| 127 | return w.popupHandler(uri) | 134 | return w.popupHandler(uri) | ||
| 128 | } | 135 | } | ||
| 129 | - | return false | 136 | + | return nil |
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | // GetNativePointer returns nil for non-CGO build (stub implementation) | |||
| 140 | + | func (w *WebView) GetNativePointer() unsafe.Pointer { //nolint:unused // Needed for interface compatibility | |||
| 141 | + | return nil | |||
| 130 | } | 142 | } | ||
| 131 | 143 | ||||
| 132 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | 144 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | ||
M pkg/webkit/webview_cgo.go +225 -27
| @@ -22,8 +22,9 @@ static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); } | |||
| 22 | 22 | // Forward declaration | |
| 23 | 23 | extern void goQuitMainLoop(); | |
| 24 | 24 | extern void goInvokeHandle(uintptr_t handle); | |
| 25 | - | extern gboolean goOnPopupRequest(unsigned long id, char* uri); | |
| 26 | - | ||
| 25 | + | extern gboolean goHandleNewWindowPolicy(unsigned long id, char* uri, int nav_type); | |
| 26 | + | extern void goHandlePopupGeometry(unsigned long id, int x, int y, int width, int height); | |
| 27 | + | extern GtkWidget* goHandleCreateWebView(unsigned long id, char* uri); | |
| 27 | 28 | static gboolean invoke_handle_cb(gpointer data) { | |
| 28 | 29 | goInvokeHandle((uintptr_t)data); | |
| 29 | 30 | return G_SOURCE_REMOVE; | |
| @@ -52,29 +53,79 @@ static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view, | |||
| 52 | 53 | GTlsCertificateFlags errors, | |
| 53 | 54 | gpointer user_data); | |
| 54 | 55 | ||
| 55 | - | // Handle create signal for _blank links - delegate to Go workspace manager | |
| 56 | - | static WebKitWebView* on_create_new_window(WebKitWebView* web_view, | |
| 57 | - | WebKitNavigationAction* navigation_action, | |
| 58 | - | gpointer user_data) { | |
| 59 | - | unsigned long view_id = (unsigned long)user_data; | |
| 60 | - | const gchar* uri = NULL; | |
| 61 | - | ||
| 62 | - | // Get the request from navigation action | |
| 63 | - | WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action); | |
| 64 | - | if (request) { | |
| 65 | - | uri = webkit_uri_request_get_uri(request); | |
| 56 | + | // Handle ready-to-show signal for popup windows to properly initialize window properties | |
| 57 | + | static void on_popup_ready_to_show(WebKitWebView* web_view, gpointer user_data) { | |
| 58 | + | // Get window properties to ensure they're properly initialized | |
| 59 | + | WebKitWindowProperties* props = webkit_web_view_get_window_properties(web_view); | |
| 60 | + | if (props) { | |
| 61 | + | // Access properties to ensure WindowFeatures are initialized | |
| 62 | + | GdkRectangle geometry; | |
| 63 | + | webkit_window_properties_get_geometry(props, &geometry); | |
| 64 | + | ||
| 65 | + | // Pass geometry to Go for workspace positioning | |
| 66 | + | unsigned long view_id = (unsigned long)user_data; | |
| 67 | + | goHandlePopupGeometry(view_id, geometry.x, geometry.y, | |
| 68 | + | geometry.width, geometry.height); | |
| 66 | 69 | } | |
| 70 | + | } | |
| 67 | 71 | ||
| 68 | - | gboolean handled = goOnPopupRequest(view_id, (char*)uri); | |
| 72 | + | // Handle create signal for popup windows - essential for window.open() support | |
| 73 | + | static GtkWidget* on_create_web_view(WebKitWebView *web_view, | |
| 74 | + | WebKitNavigationAction *navigation_action, | |
| 75 | + | gpointer user_data) { | |
| 76 | + | WebKitURIRequest *request = webkit_navigation_action_get_request(navigation_action); | |
| 77 | + | const char* uri = webkit_uri_request_get_uri(request); | |
| 78 | + | ||
| 79 | + | printf("[webkit-create] Create signal for URI: %s\n", uri ? uri : "NULL"); | |
| 80 | + | ||
| 81 | + | unsigned long view_id = (unsigned long)user_data; | |
| 82 | + | GtkWidget* new_webview = goHandleCreateWebView(view_id, (char*)uri); | |
| 69 | 83 | ||
| 70 | - | if (!handled && uri) { | |
| 71 | - | // Load in current window instead of creating new one | |
| 72 | - | webkit_web_view_load_uri(web_view, uri); | |
| 84 | + | if (new_webview) { | |
| 85 | + | printf("[webkit-create] Go handler provided WebView - using workspace pane\n"); | |
| 86 | + | return new_webview; | |
| 87 | + | } else { | |
| 88 | + | printf("[webkit-create] Go handler declined - allowing native popup window\n"); | |
| 89 | + | return NULL; // Let WebKit create native popup window | |
| 73 | 90 | } | |
| 91 | + | } | |
| 74 | 92 | ||
| 75 | - | // Return NULL to prevent new window creation | |
| 76 | - | return NULL; | |
| 93 | + | // Handle decide-policy signal for new window requests - more robust than create signal | |
| 94 | + | static gboolean on_decide_policy(WebKitWebView *web_view, | |
| 95 | + | WebKitPolicyDecision *decision, | |
| 96 | + | WebKitPolicyDecisionType type, | |
| 97 | + | gpointer user_data) { | |
| 98 | + | // Debug: log all policy decisions | |
| 99 | + | const char* type_names[] = {"NAVIGATION_ACTION", "NEW_WINDOW_ACTION", "RESPONSE"}; | |
| 100 | + | const char* type_name = (type >= 0 && type < 3) ? type_names[type] : "UNKNOWN"; | |
| 101 | + | printf("[webkit-policy] Policy decision: type=%s (%d)\n", type_name, type); | |
| 102 | + | ||
| 103 | + | if (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { | |
| 104 | + | WebKitNavigationPolicyDecision *nav_decision = | |
| 105 | + | WEBKIT_NAVIGATION_POLICY_DECISION(decision); | |
| 106 | + | WebKitNavigationAction *action = | |
| 107 | + | webkit_navigation_policy_decision_get_navigation_action(nav_decision); | |
| 108 | + | WebKitURIRequest *request = | |
| 109 | + | webkit_navigation_action_get_request(action); | |
| 110 | + | const char* uri = webkit_uri_request_get_uri(request); | |
| 111 | + | ||
| 112 | + | // Get navigation type to understand how popup was triggered | |
| 113 | + | WebKitNavigationType nav_type = webkit_navigation_action_get_navigation_type(action); | |
| 114 | + | ||
| 115 | + | // Call Go handler for popup creation | |
| 116 | + | unsigned long view_id = (unsigned long)user_data; | |
| 117 | + | gboolean handled = goHandleNewWindowPolicy(view_id, (char*)uri, nav_type); | |
| 118 | + | ||
| 119 | + | if (handled) { | |
| 120 | + | webkit_policy_decision_ignore(decision); | |
| 121 | + | } else { | |
| 122 | + | webkit_policy_decision_use(decision); | |
| 123 | + | } | |
| 124 | + | return TRUE; | |
| 125 | + | } | |
| 126 | + | return FALSE; | |
| 77 | 127 | } | |
| 128 | + | ||
| 78 | 129 | static void connect_tls_error_handler(WebKitWebView* wv); | |
| 79 | 130 | ||
| 80 | 131 | ||
| @@ -162,9 +213,46 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c | |||
| 162 | 213 | return w; | |
| 163 | 214 | } | |
| 164 | 215 | ||
| 165 | - | static void connect_create_handler(WebKitWebView* web_view, unsigned long id) { | |
| 216 | + | // Create a new WebView related to an existing one for process sharing | |
| 217 | + | static GtkWidget* new_webview_related(WebKitWebView* related_view, WebKitUserContentManager** out_ucm) { | |
| 218 | + | if (!related_view) { | |
| 219 | + | // Fallback to regular WebView creation - we need to provide the required parameters | |
| 220 | + | return new_webview_with_ucm_and_session(NULL, NULL, NULL, out_ucm, NULL); | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | // Get the network session and user content manager from the related view | |
| 224 | + | WebKitNetworkSession* related_session = webkit_web_view_get_network_session(related_view); | |
| 225 | + | WebKitUserContentManager* related_ucm = webkit_web_view_get_user_content_manager(related_view); | |
| 226 | + | ||
| 227 | + | // Create content manager if needed | |
| 228 | + | WebKitUserContentManager* u = related_ucm; | |
| 229 | + | if (!u) { | |
| 230 | + | u = webkit_user_content_manager_new(); | |
| 231 | + | } else { | |
| 232 | + | g_object_ref(u); // Add ref since we'll pass it to new WebView | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | // Create WebView with same session and content manager for process sharing | |
| 236 | + | GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW, | |
| 237 | + | "user-content-manager", u, | |
| 238 | + | "network-session", related_session, | |
| 239 | + | "related-view", related_view, | |
| 240 | + | NULL)); | |
| 241 | + | ||
| 242 | + | // Connect TLS error handler | |
| 243 | + | connect_tls_error_handler(WEBKIT_WEB_VIEW(w)); | |
| 244 | + | ||
| 245 | + | // WebView holds refs to provided objects; drop our temporary refs | |
| 246 | + | g_object_unref(u); | |
| 247 | + | if (out_ucm) { *out_ucm = u; } | |
| 248 | + | return w; | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) { | |
| 166 | 252 | if (!web_view) return; | |
| 167 | - | g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_new_window), (gpointer)id, NULL, 0); | |
| 253 | + | g_signal_connect_data(G_OBJECT(web_view), "decide-policy", G_CALLBACK(on_decide_policy), (gpointer)id, NULL, 0); | |
| 254 | + | // Connect create signal for popup window support | |
| 255 | + | g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_web_view), (gpointer)id, NULL, 0); | |
| 168 | 256 | } | |
| 169 | 257 | ||
| 170 | 258 | static gboolean gtk_prefers_dark() { | |
| @@ -944,8 +1032,8 @@ type WebView struct { | |||
| 944 | 1032 | titleHandler func(title string) | |
| 945 | 1033 | uriHandler func(uri string) | |
| 946 | 1034 | zoomHandler func(level float64) | |
| 947 | - | popupHandler func(string) bool | |
| 948 | - | memStats *memoryStats | |
| 1035 | + | popupHandler func(string) *WebView | |
| 1036 | + | memStats *memoryStats | |
| 949 | 1037 | gcTicker *time.Ticker | |
| 950 | 1038 | gcDone chan struct{} | |
| 951 | 1039 | tlsExceptions map[string]bool // host -> whether user allowed certificate exception | |
| @@ -1108,7 +1196,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1108 | 1196 | // Assign an ID for accelerator dispatch | |
| 1109 | 1197 | v.id = nextViewID() | |
| 1110 | 1198 | registerView(v.id, v) | |
| 1111 | - | C.connect_create_handler(v.native.wv, C.ulong(v.id)) | |
| 1199 | + | C.connect_policy_handler(v.native.wv, C.ulong(v.id)) | |
| 1112 | 1200 | ||
| 1113 | 1201 | // Register with global memory manager if monitoring is enabled | |
| 1114 | 1202 | if cfg.Memory.EnableMemoryMonitoring { | |
| @@ -1252,6 +1340,13 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1252 | 1340 | } | |
| 1253 | 1341 | } | |
| 1254 | 1342 | ||
| 1343 | + | // Enable JavaScript popup windows for workspace popup handling | |
| 1344 | + | settings := C.webkit_web_view_get_settings(v.native.wv) | |
| 1345 | + | if settings != nil { | |
| 1346 | + | C.webkit_settings_set_javascript_can_open_windows_automatically(settings, C.gboolean(1)) | |
| 1347 | + | log.Printf("[webkit] Enabled JavaScript popup windows") | |
| 1348 | + | } | |
| 1349 | + | ||
| 1255 | 1350 | // Apply custom User-Agent for codec negotiation | |
| 1256 | 1351 | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | |
| 1257 | 1352 | if cfg.CodecPreferences.CustomUserAgent != "" { | |
| @@ -1317,6 +1412,101 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1317 | 1412 | return v, nil | |
| 1318 | 1413 | } | |
| 1319 | 1414 | ||
| 1415 | + | // NewWebViewWithRelated creates a new WebView related to an existing one for process sharing | |
| 1416 | + | func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) { | |
| 1417 | + | if relatedView == nil || relatedView.native == nil || relatedView.native.wv == nil { | |
| 1418 | + | // Fallback to regular WebView creation | |
| 1419 | + | return NewWebView(cfg) | |
| 1420 | + | } | |
| 1421 | + | ||
| 1422 | + | log.Printf("[webkit] Creating related WebView for process sharing (CGO)") | |
| 1423 | + | if C.gtk_init_check() == 0 { | |
| 1424 | + | return nil, errors.New("failed to initialize GTK") | |
| 1425 | + | } | |
| 1426 | + | ||
| 1427 | + | if cfg == nil { | |
| 1428 | + | cfg = &Config{} | |
| 1429 | + | } | |
| 1430 | + | ||
| 1431 | + | // Create WebView related to the existing one | |
| 1432 | + | var createdUcm *C.WebKitUserContentManager | |
| 1433 | + | viewWidget := C.new_webview_related(relatedView.native.wv, &createdUcm) | |
| 1434 | + | if viewWidget == nil { | |
| 1435 | + | return nil, errors.New("failed to create related WebKitWebView") | |
| 1436 | + | } | |
| 1437 | + | ||
| 1438 | + | var win *C.GtkWidget | |
| 1439 | + | var window *Window | |
| 1440 | + | container := C.gtk_box_new(C.GtkOrientation(C.GTK_ORIENTATION_VERTICAL), 0) | |
| 1441 | + | if container == nil { | |
| 1442 | + | return nil, errors.New("failed to create GtkBox container") | |
| 1443 | + | } | |
| 1444 | + | ||
| 1445 | + | C.gtk_box_append((*C.GtkBox)(unsafe.Pointer(container)), viewWidget) | |
| 1446 | + | ||
| 1447 | + | if cfg.CreateWindow { | |
| 1448 | + | win = C.new_window() | |
| 1449 | + | if win == nil { | |
| 1450 | + | return nil, errors.New("failed to create native window") | |
| 1451 | + | } | |
| 1452 | + | C.connect_destroy_quit(win) | |
| 1453 | + | C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), container) | |
| 1454 | + | window = &Window{win: win} | |
| 1455 | + | } else { | |
| 1456 | + | window = &Window{} | |
| 1457 | + | } | |
| 1458 | + | ||
| 1459 | + | v := &WebView{ | |
| 1460 | + | config: cfg, | |
| 1461 | + | window: window, | |
| 1462 | + | zoom: cfg.ZoomDefault, | |
| 1463 | + | native: &nativeView{ | |
| 1464 | + | win: win, | |
| 1465 | + | container: container, | |
| 1466 | + | view: viewWidget, | |
| 1467 | + | wv: (*C.WebKitWebView)(unsafe.Pointer(viewWidget)), | |
| 1468 | + | ucm: createdUcm, | |
| 1469 | + | }, | |
| 1470 | + | useDomZoom: cfg.UseDomZoom, | |
| 1471 | + | domZoomSeed: cfg.ZoomDefault, | |
| 1472 | + | gcDone: make(chan struct{}), | |
| 1473 | + | tlsExceptions: make(map[string]bool), | |
| 1474 | + | } | |
| 1475 | + | ||
| 1476 | + | // Enable JavaScript popup windows for workspace popup handling | |
| 1477 | + | settings := C.webkit_web_view_get_settings(v.native.wv) | |
| 1478 | + | if settings != nil { | |
| 1479 | + | C.webkit_settings_set_javascript_can_open_windows_automatically(settings, C.gboolean(1)) | |
| 1480 | + | log.Printf("[webkit] Enabled JavaScript popup windows for related WebView") | |
| 1481 | + | } | |
| 1482 | + | ||
| 1483 | + | // Assign an ID for accelerator dispatch | |
| 1484 | + | v.id = nextViewID() | |
| 1485 | + | registerView(v.id, v) | |
| 1486 | + | C.connect_policy_handler(v.native.wv, C.ulong(v.id)) | |
| 1487 | + | ||
| 1488 | + | // Register with memory manager if enabled | |
| 1489 | + | if cfg.Memory.EnableMemoryMonitoring { | |
| 1490 | + | if globalMemoryManager == nil { | |
| 1491 | + | InitializeGlobalMemoryManager( | |
| 1492 | + | cfg.Memory.EnableMemoryMonitoring, | |
| 1493 | + | cfg.Memory.ProcessRecycleThreshold, | |
| 1494 | + | 30*time.Second, | |
| 1495 | + | ) | |
| 1496 | + | } | |
| 1497 | + | if globalMemoryManager != nil { | |
| 1498 | + | globalMemoryManager.RegisterWebView(v) | |
| 1499 | + | } | |
| 1500 | + | } | |
| 1501 | + | ||
| 1502 | + | if cfg.ZoomDefault <= 0 { | |
| 1503 | + | v.zoom = 1.0 | |
| 1504 | + | } | |
| 1505 | + | v.domZoomSeed = v.zoom | |
| 1506 | + | ||
| 1507 | + | return v, nil | |
| 1508 | + | } | |
| 1509 | + | ||
| 1320 | 1510 | func (w *WebView) LoadURL(rawURL string) error { | |
| 1321 | 1511 | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | |
| 1322 | 1512 | return ErrNotImplemented | |
| @@ -1483,7 +1673,7 @@ func (w *WebView) ReloadBypassCache() error { | |||
| 1483 | 1673 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | |
| 1484 | 1674 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | |
| 1485 | 1675 | ||
| 1486 | - | func (w *WebView) RegisterPopupHandler(cb func(string) bool) { w.popupHandler = cb } | |
| 1676 | + | func (w *WebView) RegisterPopupHandler(cb func(string) *WebView) { w.popupHandler = cb } | |
| 1487 | 1677 | ||
| 1488 | 1678 | func (w *WebView) dispatchScriptMessage(payload string) { | |
| 1489 | 1679 | if w != nil && w.msgHandler != nil { | |
| @@ -1491,11 +1681,19 @@ func (w *WebView) dispatchScriptMessage(payload string) { | |||
| 1491 | 1681 | } | |
| 1492 | 1682 | } | |
| 1493 | 1683 | ||
| 1494 | - | func (w *WebView) dispatchPopupRequest(uri string) bool { | |
| 1684 | + | func (w *WebView) dispatchPopupRequest(uri string) *WebView { | |
| 1495 | 1685 | if w != nil && w.popupHandler != nil { | |
| 1496 | 1686 | return w.popupHandler(uri) | |
| 1497 | 1687 | } | |
| 1498 | - | return false | |
| 1688 | + | return nil | |
| 1689 | + | } | |
| 1690 | + | ||
| 1691 | + | // GetNativePointer returns the native WebView pointer for CGO callbacks | |
| 1692 | + | func (w *WebView) GetNativePointer() unsafe.Pointer { | |
| 1693 | + | if w != nil && w.native != nil && w.native.wv != nil { | |
| 1694 | + | return unsafe.Pointer(w.native.wv) | |
| 1695 | + | } | |
| 1696 | + | return nil | |
| 1499 | 1697 | } | |
| 1500 | 1698 | ||
| 1501 | 1699 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | |
| @@ -22,8 +22,9 @@ static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); } | |||||
| 22 | // Forward declaration | 22 | // Forward declaration | ||
| 23 | extern void goQuitMainLoop(); | 23 | extern void goQuitMainLoop(); | ||
| 24 | extern void goInvokeHandle(uintptr_t handle); | 24 | extern void goInvokeHandle(uintptr_t handle); | ||
| 25 | - | extern gboolean goOnPopupRequest(unsigned long id, char* uri); | 25 | + | extern gboolean goHandleNewWindowPolicy(unsigned long id, char* uri, int nav_type); |
| 26 | - | 26 | + | extern void goHandlePopupGeometry(unsigned long id, int x, int y, int width, int height); | |
| 27 | + | extern GtkWidget* goHandleCreateWebView(unsigned long id, char* uri); | |||
| 27 | static gboolean invoke_handle_cb(gpointer data) { | 28 | static gboolean invoke_handle_cb(gpointer data) { | ||
| 28 | goInvokeHandle((uintptr_t)data); | 29 | goInvokeHandle((uintptr_t)data); | ||
| 29 | return G_SOURCE_REMOVE; | 30 | return G_SOURCE_REMOVE; | ||
| @@ -52,29 +53,79 @@ static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view, | |||||
| 52 | GTlsCertificateFlags errors, | 53 | GTlsCertificateFlags errors, | ||
| 53 | gpointer user_data); | 54 | gpointer user_data); | ||
| 54 | 55 | ||||
| 55 | - | // Handle create signal for _blank links - delegate to Go workspace manager | 56 | + | // Handle ready-to-show signal for popup windows to properly initialize window properties |
| 56 | - | static WebKitWebView* on_create_new_window(WebKitWebView* web_view, | 57 | + | static void on_popup_ready_to_show(WebKitWebView* web_view, gpointer user_data) { |
| 57 | - | WebKitNavigationAction* navigation_action, | 58 | + | // Get window properties to ensure they're properly initialized |
| 58 | - | gpointer user_data) { | 59 | + | WebKitWindowProperties* props = webkit_web_view_get_window_properties(web_view); |
| 59 | - | unsigned long view_id = (unsigned long)user_data; | 60 | + | if (props) { |
| 60 | - | const gchar* uri = NULL; | 61 | + | // Access properties to ensure WindowFeatures are initialized |
| 61 | - | 62 | + | GdkRectangle geometry; | |
| 62 | - | // Get the request from navigation action | 63 | + | webkit_window_properties_get_geometry(props, &geometry); |
| 63 | - | WebKitURIRequest* request = webkit_navigation_action_get_request(navigation_action); | 64 | + | |
| 64 | - | if (request) { | 65 | + | // Pass geometry to Go for workspace positioning |
| 65 | - | uri = webkit_uri_request_get_uri(request); | 66 | + | unsigned long view_id = (unsigned long)user_data; |
| 67 | + | goHandlePopupGeometry(view_id, geometry.x, geometry.y, | |||
| 68 | + | geometry.width, geometry.height); | |||
| 66 | } | 69 | } | ||
| 70 | + | } | |||
| 67 | 71 | ||||
| 68 | - | gboolean handled = goOnPopupRequest(view_id, (char*)uri); | 72 | + | // Handle create signal for popup windows - essential for window.open() support |
| 73 | + | static GtkWidget* on_create_web_view(WebKitWebView *web_view, | |||
| 74 | + | WebKitNavigationAction *navigation_action, | |||
| 75 | + | gpointer user_data) { | |||
| 76 | + | WebKitURIRequest *request = webkit_navigation_action_get_request(navigation_action); | |||
| 77 | + | const char* uri = webkit_uri_request_get_uri(request); | |||
| 78 | + | ||||
| 79 | + | printf("[webkit-create] Create signal for URI: %s\n", uri ? uri : "NULL"); | |||
| 80 | + | ||||
| 81 | + | unsigned long view_id = (unsigned long)user_data; | |||
| 82 | + | GtkWidget* new_webview = goHandleCreateWebView(view_id, (char*)uri); | |||
| 69 | 83 | ||||
| 70 | - | if (!handled && uri) { | 84 | + | if (new_webview) { |
| 71 | - | // Load in current window instead of creating new one | 85 | + | printf("[webkit-create] Go handler provided WebView - using workspace pane\n"); |
| 72 | - | webkit_web_view_load_uri(web_view, uri); | 86 | + | return new_webview; |
| 87 | + | } else { | |||
| 88 | + | printf("[webkit-create] Go handler declined - allowing native popup window\n"); | |||
| 89 | + | return NULL; // Let WebKit create native popup window | |||
| 73 | } | 90 | } | ||
| 91 | + | } | |||
| 74 | 92 | ||||
| 75 | - | // Return NULL to prevent new window creation | 93 | + | // Handle decide-policy signal for new window requests - more robust than create signal |
| 76 | - | return NULL; | 94 | + | static gboolean on_decide_policy(WebKitWebView *web_view, |
| 95 | + | WebKitPolicyDecision *decision, | |||
| 96 | + | WebKitPolicyDecisionType type, | |||
| 97 | + | gpointer user_data) { | |||
| 98 | + | // Debug: log all policy decisions | |||
| 99 | + | const char* type_names[] = {"NAVIGATION_ACTION", "NEW_WINDOW_ACTION", "RESPONSE"}; | |||
| 100 | + | const char* type_name = (type >= 0 && type < 3) ? type_names[type] : "UNKNOWN"; | |||
| 101 | + | printf("[webkit-policy] Policy decision: type=%s (%d)\n", type_name, type); | |||
| 102 | + | ||||
| 103 | + | if (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { | |||
| 104 | + | WebKitNavigationPolicyDecision *nav_decision = | |||
| 105 | + | WEBKIT_NAVIGATION_POLICY_DECISION(decision); | |||
| 106 | + | WebKitNavigationAction *action = | |||
| 107 | + | webkit_navigation_policy_decision_get_navigation_action(nav_decision); | |||
| 108 | + | WebKitURIRequest *request = | |||
| 109 | + | webkit_navigation_action_get_request(action); | |||
| 110 | + | const char* uri = webkit_uri_request_get_uri(request); | |||
| 111 | + | ||||
| 112 | + | // Get navigation type to understand how popup was triggered | |||
| 113 | + | WebKitNavigationType nav_type = webkit_navigation_action_get_navigation_type(action); | |||
| 114 | + | ||||
| 115 | + | // Call Go handler for popup creation | |||
| 116 | + | unsigned long view_id = (unsigned long)user_data; | |||
| 117 | + | gboolean handled = goHandleNewWindowPolicy(view_id, (char*)uri, nav_type); | |||
| 118 | + | ||||
| 119 | + | if (handled) { | |||
| 120 | + | webkit_policy_decision_ignore(decision); | |||
| 121 | + | } else { | |||
| 122 | + | webkit_policy_decision_use(decision); | |||
| 123 | + | } | |||
| 124 | + | return TRUE; | |||
| 125 | + | } | |||
| 126 | + | return FALSE; | |||
| 77 | } | 127 | } | ||
| 128 | + | ||||
| 78 | static void connect_tls_error_handler(WebKitWebView* wv); | 129 | static void connect_tls_error_handler(WebKitWebView* wv); | ||
| 79 | 130 | ||||
| 80 | 131 | ||||
| @@ -162,9 +213,46 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c | |||||
| 162 | return w; | 213 | return w; | ||
| 163 | } | 214 | } | ||
| 164 | 215 | ||||
| 165 | - | static void connect_create_handler(WebKitWebView* web_view, unsigned long id) { | 216 | + | // Create a new WebView related to an existing one for process sharing |
| 217 | + | static GtkWidget* new_webview_related(WebKitWebView* related_view, WebKitUserContentManager** out_ucm) { | |||
| 218 | + | if (!related_view) { | |||
| 219 | + | // Fallback to regular WebView creation - we need to provide the required parameters | |||
| 220 | + | return new_webview_with_ucm_and_session(NULL, NULL, NULL, out_ucm, NULL); | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | // Get the network session and user content manager from the related view | |||
| 224 | + | WebKitNetworkSession* related_session = webkit_web_view_get_network_session(related_view); | |||
| 225 | + | WebKitUserContentManager* related_ucm = webkit_web_view_get_user_content_manager(related_view); | |||
| 226 | + | ||||
| 227 | + | // Create content manager if needed | |||
| 228 | + | WebKitUserContentManager* u = related_ucm; | |||
| 229 | + | if (!u) { | |||
| 230 | + | u = webkit_user_content_manager_new(); | |||
| 231 | + | } else { | |||
| 232 | + | g_object_ref(u); // Add ref since we'll pass it to new WebView | |||
| 233 | + | } | |||
| 234 | + | ||||
| 235 | + | // Create WebView with same session and content manager for process sharing | |||
| 236 | + | GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW, | |||
| 237 | + | "user-content-manager", u, | |||
| 238 | + | "network-session", related_session, | |||
| 239 | + | "related-view", related_view, | |||
| 240 | + | NULL)); | |||
| 241 | + | ||||
| 242 | + | // Connect TLS error handler | |||
| 243 | + | connect_tls_error_handler(WEBKIT_WEB_VIEW(w)); | |||
| 244 | + | ||||
| 245 | + | // WebView holds refs to provided objects; drop our temporary refs | |||
| 246 | + | g_object_unref(u); | |||
| 247 | + | if (out_ucm) { *out_ucm = u; } | |||
| 248 | + | return w; | |||
| 249 | + | } | |||
| 250 | + | ||||
| 251 | + | static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) { | |||
| 166 | if (!web_view) return; | 252 | if (!web_view) return; | ||
| 167 | - | g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_new_window), (gpointer)id, NULL, 0); | 253 | + | g_signal_connect_data(G_OBJECT(web_view), "decide-policy", G_CALLBACK(on_decide_policy), (gpointer)id, NULL, 0); |
| 254 | + | // Connect create signal for popup window support | |||
| 255 | + | g_signal_connect_data(G_OBJECT(web_view), "create", G_CALLBACK(on_create_web_view), (gpointer)id, NULL, 0); | |||
| 168 | } | 256 | } | ||
| 169 | 257 | ||||
| 170 | static gboolean gtk_prefers_dark() { | 258 | static gboolean gtk_prefers_dark() { | ||
| @@ -944,8 +1032,8 @@ type WebView struct { | |||||
| 944 | titleHandler func(title string) | 1032 | titleHandler func(title string) | ||
| 945 | uriHandler func(uri string) | 1033 | uriHandler func(uri string) | ||
| 946 | zoomHandler func(level float64) | 1034 | zoomHandler func(level float64) | ||
| 947 | - | popupHandler func(string) bool | 1035 | + | popupHandler func(string) *WebView |
| 948 | - | memStats *memoryStats | 1036 | + | memStats *memoryStats |
| 949 | gcTicker *time.Ticker | 1037 | gcTicker *time.Ticker | ||
| 950 | gcDone chan struct{} | 1038 | gcDone chan struct{} | ||
| 951 | tlsExceptions map[string]bool // host -> whether user allowed certificate exception | 1039 | tlsExceptions map[string]bool // host -> whether user allowed certificate exception | ||
| @@ -1108,7 +1196,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1108 | // Assign an ID for accelerator dispatch | 1196 | // Assign an ID for accelerator dispatch | ||
| 1109 | v.id = nextViewID() | 1197 | v.id = nextViewID() | ||
| 1110 | registerView(v.id, v) | 1198 | registerView(v.id, v) | ||
| 1111 | - | C.connect_create_handler(v.native.wv, C.ulong(v.id)) | 1199 | + | C.connect_policy_handler(v.native.wv, C.ulong(v.id)) |
| 1112 | 1200 | ||||
| 1113 | // Register with global memory manager if monitoring is enabled | 1201 | // Register with global memory manager if monitoring is enabled | ||
| 1114 | if cfg.Memory.EnableMemoryMonitoring { | 1202 | if cfg.Memory.EnableMemoryMonitoring { | ||
| @@ -1252,6 +1340,13 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1252 | } | 1340 | } | ||
| 1253 | } | 1341 | } | ||
| 1254 | 1342 | ||||
| 1343 | + | // Enable JavaScript popup windows for workspace popup handling | |||
| 1344 | + | settings := C.webkit_web_view_get_settings(v.native.wv) | |||
| 1345 | + | if settings != nil { | |||
| 1346 | + | C.webkit_settings_set_javascript_can_open_windows_automatically(settings, C.gboolean(1)) | |||
| 1347 | + | log.Printf("[webkit] Enabled JavaScript popup windows") | |||
| 1348 | + | } | |||
| 1349 | + | ||||
| 1255 | // Apply custom User-Agent for codec negotiation | 1350 | // Apply custom User-Agent for codec negotiation | ||
| 1256 | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | 1351 | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | ||
| 1257 | if cfg.CodecPreferences.CustomUserAgent != "" { | 1352 | if cfg.CodecPreferences.CustomUserAgent != "" { | ||
| @@ -1317,6 +1412,101 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1317 | return v, nil | 1412 | return v, nil | ||
| 1318 | } | 1413 | } | ||
| 1319 | 1414 | ||||
| 1415 | + | // NewWebViewWithRelated creates a new WebView related to an existing one for process sharing | |||
| 1416 | + | func NewWebViewWithRelated(cfg *Config, relatedView *WebView) (*WebView, error) { | |||
| 1417 | + | if relatedView == nil || relatedView.native == nil || relatedView.native.wv == nil { | |||
| 1418 | + | // Fallback to regular WebView creation | |||
| 1419 | + | return NewWebView(cfg) | |||
| 1420 | + | } | |||
| 1421 | + | ||||
| 1422 | + | log.Printf("[webkit] Creating related WebView for process sharing (CGO)") | |||
| 1423 | + | if C.gtk_init_check() == 0 { | |||
| 1424 | + | return nil, errors.New("failed to initialize GTK") | |||
| 1425 | + | } | |||
| 1426 | + | ||||
| 1427 | + | if cfg == nil { | |||
| 1428 | + | cfg = &Config{} | |||
| 1429 | + | } | |||
| 1430 | + | ||||
| 1431 | + | // Create WebView related to the existing one | |||
| 1432 | + | var createdUcm *C.WebKitUserContentManager | |||
| 1433 | + | viewWidget := C.new_webview_related(relatedView.native.wv, &createdUcm) | |||
| 1434 | + | if viewWidget == nil { | |||
| 1435 | + | return nil, errors.New("failed to create related WebKitWebView") | |||
| 1436 | + | } | |||
| 1437 | + | ||||
| 1438 | + | var win *C.GtkWidget | |||
| 1439 | + | var window *Window | |||
| 1440 | + | container := C.gtk_box_new(C.GtkOrientation(C.GTK_ORIENTATION_VERTICAL), 0) | |||
| 1441 | + | if container == nil { | |||
| 1442 | + | return nil, errors.New("failed to create GtkBox container") | |||
| 1443 | + | } | |||
| 1444 | + | ||||
| 1445 | + | C.gtk_box_append((*C.GtkBox)(unsafe.Pointer(container)), viewWidget) | |||
| 1446 | + | ||||
| 1447 | + | if cfg.CreateWindow { | |||
| 1448 | + | win = C.new_window() | |||
| 1449 | + | if win == nil { | |||
| 1450 | + | return nil, errors.New("failed to create native window") | |||
| 1451 | + | } | |||
| 1452 | + | C.connect_destroy_quit(win) | |||
| 1453 | + | C.gtk_window_set_child((*C.GtkWindow)(unsafe.Pointer(win)), container) | |||
| 1454 | + | window = &Window{win: win} | |||
| 1455 | + | } else { | |||
| 1456 | + | window = &Window{} | |||
| 1457 | + | } | |||
| 1458 | + | ||||
| 1459 | + | v := &WebView{ | |||
| 1460 | + | config: cfg, | |||
| 1461 | + | window: window, | |||
| 1462 | + | zoom: cfg.ZoomDefault, | |||
| 1463 | + | native: &nativeView{ | |||
| 1464 | + | win: win, | |||
| 1465 | + | container: container, | |||
| 1466 | + | view: viewWidget, | |||
| 1467 | + | wv: (*C.WebKitWebView)(unsafe.Pointer(viewWidget)), | |||
| 1468 | + | ucm: createdUcm, | |||
| 1469 | + | }, | |||
| 1470 | + | useDomZoom: cfg.UseDomZoom, | |||
| 1471 | + | domZoomSeed: cfg.ZoomDefault, | |||
| 1472 | + | gcDone: make(chan struct{}), | |||
| 1473 | + | tlsExceptions: make(map[string]bool), | |||
| 1474 | + | } | |||
| 1475 | + | ||||
| 1476 | + | // Enable JavaScript popup windows for workspace popup handling | |||
| 1477 | + | settings := C.webkit_web_view_get_settings(v.native.wv) | |||
| 1478 | + | if settings != nil { | |||
| 1479 | + | C.webkit_settings_set_javascript_can_open_windows_automatically(settings, C.gboolean(1)) | |||
| 1480 | + | log.Printf("[webkit] Enabled JavaScript popup windows for related WebView") | |||
| 1481 | + | } | |||
| 1482 | + | ||||
| 1483 | + | // Assign an ID for accelerator dispatch | |||
| 1484 | + | v.id = nextViewID() | |||
| 1485 | + | registerView(v.id, v) | |||
| 1486 | + | C.connect_policy_handler(v.native.wv, C.ulong(v.id)) | |||
| 1487 | + | ||||
| 1488 | + | // Register with memory manager if enabled | |||
| 1489 | + | if cfg.Memory.EnableMemoryMonitoring { | |||
| 1490 | + | if globalMemoryManager == nil { | |||
| 1491 | + | InitializeGlobalMemoryManager( | |||
| 1492 | + | cfg.Memory.EnableMemoryMonitoring, | |||
| 1493 | + | cfg.Memory.ProcessRecycleThreshold, | |||
| 1494 | + | 30*time.Second, | |||
| 1495 | + | ) | |||
| 1496 | + | } | |||
| 1497 | + | if globalMemoryManager != nil { | |||
| 1498 | + | globalMemoryManager.RegisterWebView(v) | |||
| 1499 | + | } | |||
| 1500 | + | } | |||
| 1501 | + | ||||
| 1502 | + | if cfg.ZoomDefault <= 0 { | |||
| 1503 | + | v.zoom = 1.0 | |||
| 1504 | + | } | |||
| 1505 | + | v.domZoomSeed = v.zoom | |||
| 1506 | + | ||||
| 1507 | + | return v, nil | |||
| 1508 | + | } | |||
| 1509 | + | ||||
| 1320 | func (w *WebView) LoadURL(rawURL string) error { | 1510 | func (w *WebView) LoadURL(rawURL string) error { | ||
| 1321 | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | 1511 | if w == nil || w.destroyed || w.native == nil || w.native.wv == nil { | ||
| 1322 | return ErrNotImplemented | 1512 | return ErrNotImplemented | ||
| @@ -1483,7 +1673,7 @@ func (w *WebView) ReloadBypassCache() error { | |||||
| 1483 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | 1673 | // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message. | ||
| 1484 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | 1674 | func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb } | ||
| 1485 | 1675 | ||||
| 1486 | - | func (w *WebView) RegisterPopupHandler(cb func(string) bool) { w.popupHandler = cb } | 1676 | + | func (w *WebView) RegisterPopupHandler(cb func(string) *WebView) { w.popupHandler = cb } |
| 1487 | 1677 | ||||
| 1488 | func (w *WebView) dispatchScriptMessage(payload string) { | 1678 | func (w *WebView) dispatchScriptMessage(payload string) { | ||
| 1489 | if w != nil && w.msgHandler != nil { | 1679 | if w != nil && w.msgHandler != nil { | ||
| @@ -1491,11 +1681,19 @@ func (w *WebView) dispatchScriptMessage(payload string) { | |||||
| 1491 | } | 1681 | } | ||
| 1492 | } | 1682 | } | ||
| 1493 | 1683 | ||||
| 1494 | - | func (w *WebView) dispatchPopupRequest(uri string) bool { | 1684 | + | func (w *WebView) dispatchPopupRequest(uri string) *WebView { |
| 1495 | if w != nil && w.popupHandler != nil { | 1685 | if w != nil && w.popupHandler != nil { | ||
| 1496 | return w.popupHandler(uri) | 1686 | return w.popupHandler(uri) | ||
| 1497 | } | 1687 | } | ||
| 1498 | - | return false | 1688 | + | return nil |
| 1689 | + | } | |||
| 1690 | + | ||||
| 1691 | + | // GetNativePointer returns the native WebView pointer for CGO callbacks | |||
| 1692 | + | func (w *WebView) GetNativePointer() unsafe.Pointer { | |||
| 1693 | + | if w != nil && w.native != nil && w.native.wv != nil { | |||
| 1694 | + | return unsafe.Pointer(w.native.wv) | |||
| 1695 | + | } | |||
| 1696 | + | return nil | |||
| 1499 | } | 1697 | } | ||
| 1500 | 1698 | ||||
| 1501 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | 1699 | // RegisterTitleChangedHandler registers a callback invoked when the page title changes. | ||