refactor(browser): implement native popup lifecycle in workspace
Major refactoring to use WebKit's create/ready-to-show/close signals: - setupPopupHandling(): connects to WebKit create signal - handlePopupReadyToShow(): inserts pane when WebKit ready - handlePopupClose(): responds to WebKit close signal - Remove deprecated HandlePopup() and manual lifecycle code - Replace paneDeduplicator with pendingPopups map - Extract syncPanedDivider() helper for paned position syncing - Add popupID field to paneNode for lifecycle tracking This eliminates SIGSEGV crashes by respecting WebKit's popup lifecycle instead of bypassing it with manual creation.
- commit
- 4c93f210aee493f594ad17153acbe4b72825b25f
- author
- bamen · · 2025-10-12 07:05 UTC
- parent
- 504ed69
- signature
- unsigned
5 files · +457 -687
M internal/app/browser/webview.go +17 -14
| @@ -200,21 +200,14 @@ func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) { | |||
| 200 | 200 | pane.messageHandler.SetNavigationController(pane.navigationController) | |
| 201 | 201 | } | |
| 202 | 202 | ||
| 203 | - | pane.webView.RegisterPopupHandler(func(uri string) *webkit.WebView { | |
| 204 | - | if app.workspace != nil { | |
| 205 | - | return app.workspace.HandlePopup(pane.webView, uri) | |
| 206 | - | } | |
| 207 | - | ||
| 208 | - | // No workspace manager - just navigate in current pane | |
| 209 | - | if uri != "" && pane.navigationController != nil { | |
| 210 | - | go func() { | |
| 211 | - | if err := pane.navigationController.NavigateToURL(uri); err != nil { | |
| 212 | - | log.Printf("[webview] failed to navigate to URL %s: %v", uri, err) | |
| 213 | - | } | |
| 214 | - | }() | |
| 203 | + | // Setup WebKit-native popup lifecycle using create/ready-to-show/close signals | |
| 204 | + | if app.workspace != nil { | |
| 205 | + | node := app.workspace.GetNodeForWebView(pane.webView) | |
| 206 | + | if node != nil { | |
| 207 | + | app.workspace.setupPopupHandling(pane.webView, node) | |
| 208 | + | log.Printf("[webview] Setup native popup handling for WebView ID: %d", pane.webView.ID()) | |
| 215 | 209 | } | |
| 216 | - | return nil | |
| 217 | - | }) | |
| 210 | + | } | |
| 218 | 211 | } | |
| 219 | 212 | ||
| 220 | 213 | // createWebView creates and configures the WebView | |
| @@ -252,6 +245,16 @@ func (app *BrowserApp) createWebView() error { | |||
| 252 | 245 | // CRITICAL: Register navigation handler for the root webview | |
| 253 | 246 | // This is needed because buildPane() couldn't register it when workspace was nil | |
| 254 | 247 | app.workspace.RegisterNavigationHandler(view) | |
| 248 | + | ||
| 249 | + | // CRITICAL: Setup popup handling for the root webview | |
| 250 | + | // This must be done after workspace is created so WebKit's create signal is connected | |
| 251 | + | node := app.workspace.GetNodeForWebView(view) | |
| 252 | + | if node != nil { | |
| 253 | + | app.workspace.setupPopupHandling(view, node) | |
| 254 | + | log.Printf("[webview] Setup native popup handling for root WebView ID: %d", view.ID()) | |
| 255 | + | } else { | |
| 256 | + | log.Printf("[webview] WARNING: Could not find node for root WebView ID: %d", view.ID()) | |
| 257 | + | } | |
| 255 | 258 | } | |
| 256 | 259 | ||
| 257 | 260 | // Initialize window-level global shortcuts AFTER workspace is set up | |
| @@ -200,21 +200,14 @@ func (app *BrowserApp) attachPaneHandlers(pane *BrowserPane) { | |||||
| 200 | pane.messageHandler.SetNavigationController(pane.navigationController) | 200 | pane.messageHandler.SetNavigationController(pane.navigationController) | ||
| 201 | } | 201 | } | ||
| 202 | 202 | ||||
| 203 | - | pane.webView.RegisterPopupHandler(func(uri string) *webkit.WebView { | 203 | + | // Setup WebKit-native popup lifecycle using create/ready-to-show/close signals |
| 204 | - | if app.workspace != nil { | 204 | + | if app.workspace != nil { |
| 205 | - | return app.workspace.HandlePopup(pane.webView, uri) | 205 | + | node := app.workspace.GetNodeForWebView(pane.webView) |
| 206 | - | } | 206 | + | if node != nil { |
| 207 | - | 207 | + | app.workspace.setupPopupHandling(pane.webView, node) | |
| 208 | - | // No workspace manager - just navigate in current pane | 208 | + | log.Printf("[webview] Setup native popup handling for WebView ID: %d", pane.webView.ID()) |
| 209 | - | if uri != "" && pane.navigationController != nil { | |||
| 210 | - | go func() { | |||
| 211 | - | if err := pane.navigationController.NavigateToURL(uri); err != nil { | |||
| 212 | - | log.Printf("[webview] failed to navigate to URL %s: %v", uri, err) | |||
| 213 | - | } | |||
| 214 | - | }() | |||
| 215 | } | 209 | } | ||
| 216 | - | return nil | 210 | + | } |
| 217 | - | }) | |||
| 218 | } | 211 | } | ||
| 219 | 212 | ||||
| 220 | // createWebView creates and configures the WebView | 213 | // createWebView creates and configures the WebView | ||
| @@ -252,6 +245,16 @@ func (app *BrowserApp) createWebView() error { | |||||
| 252 | // CRITICAL: Register navigation handler for the root webview | 245 | // CRITICAL: Register navigation handler for the root webview | ||
| 253 | // This is needed because buildPane() couldn't register it when workspace was nil | 246 | // This is needed because buildPane() couldn't register it when workspace was nil | ||
| 254 | app.workspace.RegisterNavigationHandler(view) | 247 | app.workspace.RegisterNavigationHandler(view) | ||
| 248 | + | ||||
| 249 | + | // CRITICAL: Setup popup handling for the root webview | |||
| 250 | + | // This must be done after workspace is created so WebKit's create signal is connected | |||
| 251 | + | node := app.workspace.GetNodeForWebView(view) | |||
| 252 | + | if node != nil { | |||
| 253 | + | app.workspace.setupPopupHandling(view, node) | |||
| 254 | + | log.Printf("[webview] Setup native popup handling for root WebView ID: %d", view.ID()) | |||
| 255 | + | } else { | |||
| 256 | + | log.Printf("[webview] WARNING: Could not find node for root WebView ID: %d", view.ID()) | |||
| 257 | + | } | |||
| 255 | } | 258 | } | ||
| 256 | 259 | ||||
| 257 | // Initialize window-level global shortcuts AFTER workspace is set up | 260 | // Initialize window-level global shortcuts AFTER workspace is set up | ||
M internal/app/browser/workspace_manager.go +11 -63
| @@ -43,7 +43,8 @@ type WorkspaceManager struct { | |||
| 43 | 43 | lastStackOperation time.Time // When a stack operation was last performed | |
| 44 | 44 | ||
| 45 | 45 | // NEW: Pane creation deduplicator | |
| 46 | - | paneDeduplicator *messaging.PaneRequestDeduplicator | |
| 46 | + | // Popups waiting for ready-to-show lifecycle callback | |
| 47 | + | pendingPopups map[uint64]*pendingPopup | |
| 47 | 48 | ||
| 48 | 49 | // Specialized managers for different pane operations | |
| 49 | 50 | stackedPaneManager *StackedPaneManager | |
| @@ -77,14 +78,14 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||
| 77 | 78 | debugLevel := getDebugLevel() | |
| 78 | 79 | ||
| 79 | 80 | manager := &WorkspaceManager{ | |
| 80 | - | app: app, | |
| 81 | - | window: rootPane.webView.Window(), | |
| 82 | - | viewToNode: make(map[*webkit.WebView]*paneNode), | |
| 83 | - | lastSplitMsg: make(map[*webkit.WebView]time.Time), | |
| 84 | - | lastExitMsg: make(map[*webkit.WebView]time.Time), | |
| 85 | - | paneDeduplicator: messaging.NewPaneRequestDeduplicator(), | |
| 86 | - | focusDebounce: 150 * time.Millisecond, | |
| 87 | - | debugLevel: debugLevel, | |
| 81 | + | app: app, | |
| 82 | + | window: rootPane.webView.Window(), | |
| 83 | + | viewToNode: make(map[*webkit.WebView]*paneNode), | |
| 84 | + | lastSplitMsg: make(map[*webkit.WebView]time.Time), | |
| 85 | + | lastExitMsg: make(map[*webkit.WebView]time.Time), | |
| 86 | + | pendingPopups: make(map[uint64]*pendingPopup), | |
| 87 | + | focusDebounce: 150 * time.Millisecond, | |
| 88 | + | debugLevel: debugLevel, | |
| 88 | 89 | } | |
| 89 | 90 | ||
| 90 | 91 | // Initialize validation components (opt-in based on debug level) | |
| @@ -325,60 +326,7 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa | |||
| 325 | 326 | } | |
| 326 | 327 | wm.clonePaneState(node, newNode) | |
| 327 | 328 | case "create-pane": | |
| 328 | - | log.Printf("[workspace] create-pane requested: url=%s action=%s requestId=%s", msg.URL, msg.Action, msg.RequestID) | |
| 329 | - | ||
| 330 | - | if msg.URL == "" { | |
| 331 | - | log.Printf("[workspace] create-pane: empty URL, ignoring") | |
| 332 | - | break | |
| 333 | - | } | |
| 334 | - | ||
| 335 | - | // NEW: Get WebView ID for deduplication | |
| 336 | - | webViewID := "unknown" | |
| 337 | - | if source != nil { | |
| 338 | - | // Try to get a unique identifier for the WebView | |
| 339 | - | webViewID = fmt.Sprintf("%p", source) | |
| 340 | - | } | |
| 341 | - | ||
| 342 | - | // NEW: Create intent for deduplication check | |
| 343 | - | intent := &messaging.WindowIntent{ | |
| 344 | - | URL: msg.URL, | |
| 345 | - | WindowType: msg.Action, | |
| 346 | - | Timestamp: time.Now().UnixMilli(), | |
| 347 | - | RequestID: msg.RequestID, | |
| 348 | - | UserTriggered: true, | |
| 349 | - | } | |
| 350 | - | ||
| 351 | - | // NEW: Check for duplicates | |
| 352 | - | if isDup, reason := wm.paneDeduplicator.IsDuplicate(intent, webViewID); isDup { | |
| 353 | - | log.Printf("[workspace] create-pane BLOCKED: %s", reason) | |
| 354 | - | break | |
| 355 | - | } | |
| 356 | - | ||
| 357 | - | // Use the existing methods to handle tab vs popup creation | |
| 358 | - | switch strings.ToLower(msg.Action) { | |
| 359 | - | case "tab": | |
| 360 | - | newView := wm.handleIntentAsTab(node, msg.URL, intent) | |
| 361 | - | if newView != nil { | |
| 362 | - | log.Printf("[workspace] create-pane: tab created successfully") | |
| 363 | - | } else { | |
| 364 | - | log.Printf("[workspace] create-pane: failed to create tab") | |
| 365 | - | } | |
| 366 | - | case "popup": | |
| 367 | - | newView := wm.handleIntentAsPopup(node, msg.URL, intent) | |
| 368 | - | if newView != nil { | |
| 369 | - | log.Printf("[workspace] create-pane: popup created successfully") | |
| 370 | - | } else { | |
| 371 | - | log.Printf("[workspace] create-pane: failed to create popup") | |
| 372 | - | } | |
| 373 | - | default: | |
| 374 | - | log.Printf("[workspace] create-pane: unknown action '%s', defaulting to tab", msg.Action) | |
| 375 | - | newView := wm.handleIntentAsTab(node, msg.URL, intent) | |
| 376 | - | if newView != nil { | |
| 377 | - | log.Printf("[workspace] create-pane: default tab created successfully") | |
| 378 | - | } else { | |
| 379 | - | log.Printf("[workspace] create-pane: failed to create default tab") | |
| 380 | - | } | |
| 381 | - | } | |
| 329 | + | log.Printf("[workspace] create-pane message ignored (native popup lifecycle enabled)") | |
| 382 | 330 | case "close-popup": | |
| 383 | 331 | log.Printf("[workspace] close-popup requested: webviewId=%s reason=%s", msg.WebViewID, msg.Reason) | |
| 384 | 332 | ||
| @@ -43,7 +43,8 @@ type WorkspaceManager struct { | |||||
| 43 | lastStackOperation time.Time // When a stack operation was last performed | 43 | lastStackOperation time.Time // When a stack operation was last performed | ||
| 44 | 44 | ||||
| 45 | // NEW: Pane creation deduplicator | 45 | // NEW: Pane creation deduplicator | ||
| 46 | - | paneDeduplicator *messaging.PaneRequestDeduplicator | 46 | + | // Popups waiting for ready-to-show lifecycle callback |
| 47 | + | pendingPopups map[uint64]*pendingPopup | |||
| 47 | 48 | ||||
| 48 | // Specialized managers for different pane operations | 49 | // Specialized managers for different pane operations | ||
| 49 | stackedPaneManager *StackedPaneManager | 50 | stackedPaneManager *StackedPaneManager | ||
| @@ -77,14 +78,14 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||||
| 77 | debugLevel := getDebugLevel() | 78 | debugLevel := getDebugLevel() | ||
| 78 | 79 | ||||
| 79 | manager := &WorkspaceManager{ | 80 | manager := &WorkspaceManager{ | ||
| 80 | - | app: app, | 81 | + | app: app, |
| 81 | - | window: rootPane.webView.Window(), | 82 | + | window: rootPane.webView.Window(), |
| 82 | - | viewToNode: make(map[*webkit.WebView]*paneNode), | 83 | + | viewToNode: make(map[*webkit.WebView]*paneNode), |
| 83 | - | lastSplitMsg: make(map[*webkit.WebView]time.Time), | 84 | + | lastSplitMsg: make(map[*webkit.WebView]time.Time), |
| 84 | - | lastExitMsg: make(map[*webkit.WebView]time.Time), | 85 | + | lastExitMsg: make(map[*webkit.WebView]time.Time), |
| 85 | - | paneDeduplicator: messaging.NewPaneRequestDeduplicator(), | 86 | + | pendingPopups: make(map[uint64]*pendingPopup), |
| 86 | - | focusDebounce: 150 * time.Millisecond, | 87 | + | focusDebounce: 150 * time.Millisecond, |
| 87 | - | debugLevel: debugLevel, | 88 | + | debugLevel: debugLevel, |
| 88 | } | 89 | } | ||
| 89 | 90 | ||||
| 90 | // Initialize validation components (opt-in based on debug level) | 91 | // Initialize validation components (opt-in based on debug level) | ||
| @@ -325,60 +326,7 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa | |||||
| 325 | } | 326 | } | ||
| 326 | wm.clonePaneState(node, newNode) | 327 | wm.clonePaneState(node, newNode) | ||
| 327 | case "create-pane": | 328 | case "create-pane": | ||
| 328 | - | log.Printf("[workspace] create-pane requested: url=%s action=%s requestId=%s", msg.URL, msg.Action, msg.RequestID) | 329 | + | log.Printf("[workspace] create-pane message ignored (native popup lifecycle enabled)") |
| 329 | - | ||||
| 330 | - | if msg.URL == "" { | |||
| 331 | - | log.Printf("[workspace] create-pane: empty URL, ignoring") | |||
| 332 | - | break | |||
| 333 | - | } | |||
| 334 | - | ||||
| 335 | - | // NEW: Get WebView ID for deduplication | |||
| 336 | - | webViewID := "unknown" | |||
| 337 | - | if source != nil { | |||
| 338 | - | // Try to get a unique identifier for the WebView | |||
| 339 | - | webViewID = fmt.Sprintf("%p", source) | |||
| 340 | - | } | |||
| 341 | - | ||||
| 342 | - | // NEW: Create intent for deduplication check | |||
| 343 | - | intent := &messaging.WindowIntent{ | |||
| 344 | - | URL: msg.URL, | |||
| 345 | - | WindowType: msg.Action, | |||
| 346 | - | Timestamp: time.Now().UnixMilli(), | |||
| 347 | - | RequestID: msg.RequestID, | |||
| 348 | - | UserTriggered: true, | |||
| 349 | - | } | |||
| 350 | - | ||||
| 351 | - | // NEW: Check for duplicates | |||
| 352 | - | if isDup, reason := wm.paneDeduplicator.IsDuplicate(intent, webViewID); isDup { | |||
| 353 | - | log.Printf("[workspace] create-pane BLOCKED: %s", reason) | |||
| 354 | - | break | |||
| 355 | - | } | |||
| 356 | - | ||||
| 357 | - | // Use the existing methods to handle tab vs popup creation | |||
| 358 | - | switch strings.ToLower(msg.Action) { | |||
| 359 | - | case "tab": | |||
| 360 | - | newView := wm.handleIntentAsTab(node, msg.URL, intent) | |||
| 361 | - | if newView != nil { | |||
| 362 | - | log.Printf("[workspace] create-pane: tab created successfully") | |||
| 363 | - | } else { | |||
| 364 | - | log.Printf("[workspace] create-pane: failed to create tab") | |||
| 365 | - | } | |||
| 366 | - | case "popup": | |||
| 367 | - | newView := wm.handleIntentAsPopup(node, msg.URL, intent) | |||
| 368 | - | if newView != nil { | |||
| 369 | - | log.Printf("[workspace] create-pane: popup created successfully") | |||
| 370 | - | } else { | |||
| 371 | - | log.Printf("[workspace] create-pane: failed to create popup") | |||
| 372 | - | } | |||
| 373 | - | default: | |||
| 374 | - | log.Printf("[workspace] create-pane: unknown action '%s', defaulting to tab", msg.Action) | |||
| 375 | - | newView := wm.handleIntentAsTab(node, msg.URL, intent) | |||
| 376 | - | if newView != nil { | |||
| 377 | - | log.Printf("[workspace] create-pane: default tab created successfully") | |||
| 378 | - | } else { | |||
| 379 | - | log.Printf("[workspace] create-pane: failed to create default tab") | |||
| 380 | - | } | |||
| 381 | - | } | |||
| 382 | case "close-popup": | 330 | case "close-popup": | ||
| 383 | log.Printf("[workspace] close-popup requested: webviewId=%s reason=%s", msg.WebViewID, msg.Reason) | 331 | log.Printf("[workspace] close-popup requested: webviewId=%s reason=%s", msg.WebViewID, msg.Reason) | ||
| 384 | 332 | ||||
M internal/app/browser/workspace_pane_ops.go +79 -42
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "log" | |
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/dumber/pkg/webkit" | |
| 9 | + | "github.com/diamondburned/gotk4/pkg/gdk/v4" | |
| 9 | 10 | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| @@ -497,27 +498,8 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 497 | 498 | return false | |
| 498 | 499 | }, split) | |
| 499 | 500 | ||
| 500 | - | // CRITICAL: Set initial 50/50 split position after showing paned | |
| 501 | - | // GTK needs the widget to be realized before we can set position based on allocation | |
| 502 | - | // Schedule this to run after GTK has allocated space to the paned | |
| 503 | - | wm.scheduleIdleGuarded(func() bool { | |
| 504 | - | if paned == nil { | |
| 505 | - | return false | |
| 506 | - | } | |
| 507 | - | // Get paned allocation to calculate 50% position | |
| 508 | - | alloc := paned.Allocation() | |
| 509 | - | var splitPos int | |
| 510 | - | if orientation == gtk.OrientationHorizontal { | |
| 511 | - | splitPos = alloc.Width() / 2 | |
| 512 | - | } else { | |
| 513 | - | splitPos = alloc.Height() / 2 | |
| 514 | - | } | |
| 515 | - | if splitPos > 0 { | |
| 516 | - | paned.SetPosition(splitPos) | |
| 517 | - | log.Printf("[workspace] Set initial paned position to 50%%: %d (orientation=%d, size=%dx%d)", splitPos, orientation, alloc.Width(), alloc.Height()) | |
| 518 | - | } | |
| 519 | - | return false | |
| 520 | - | }, split) | |
| 501 | + | // Synchronize the divider once the paned has a meaningful allocation. | |
| 502 | + | wm.syncPanedDivider(paned, orientation, split) | |
| 521 | 503 | ||
| 522 | 504 | // CRITICAL: Show the split target container and force GTK to recreate rendering surface after reparenting | |
| 523 | 505 | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | |
| @@ -785,27 +767,8 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||
| 785 | 767 | ||
| 786 | 768 | paned.Show() | |
| 787 | 769 | ||
| 788 | - | // CRITICAL: Set initial 50/50 split position after showing paned | |
| 789 | - | // GTK needs the widget to be realized before we can set position based on allocation | |
| 790 | - | // Schedule this to run after GTK has allocated space to the paned | |
| 791 | - | wm.scheduleIdleGuarded(func() bool { | |
| 792 | - | if paned == nil { | |
| 793 | - | return false | |
| 794 | - | } | |
| 795 | - | // Get paned allocation to calculate 50% position | |
| 796 | - | alloc := paned.Allocation() | |
| 797 | - | var splitPos int | |
| 798 | - | if orientation == gtk.OrientationHorizontal { | |
| 799 | - | splitPos = alloc.Width() / 2 | |
| 800 | - | } else { | |
| 801 | - | splitPos = alloc.Height() / 2 | |
| 802 | - | } | |
| 803 | - | if splitPos > 0 { | |
| 804 | - | paned.SetPosition(splitPos) | |
| 805 | - | log.Printf("[workspace] Set initial paned position to 50%%: %d (orientation=%d, size=%dx%d)", splitPos, orientation, alloc.Width(), alloc.Height()) | |
| 806 | - | } | |
| 807 | - | return false | |
| 808 | - | }, split) | |
| 770 | + | // Synchronize the divider once the paned has a meaningful allocation. | |
| 771 | + | wm.syncPanedDivider(paned, orientation, split) | |
| 809 | 772 | ||
| 810 | 773 | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | |
| 811 | 774 | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | |
| @@ -1381,3 +1344,77 @@ func (wm *WorkspaceManager) closeStackedPaneCompat(node *paneNode) (*paneNode, e | |||
| 1381 | 1344 | ||
| 1382 | 1345 | return stack, nil | |
| 1383 | 1346 | } | |
| 1347 | + | ||
| 1348 | + | // syncPanedDivider waits until a GtkPaned has a meaningful allocation before snapping | |
| 1349 | + | // the divider to an even split. GTK often reports near-zero allocations for a few frames | |
| 1350 | + | // after insertion, so we retry on map/tick/idle until the size stabilizes. | |
| 1351 | + | func (wm *WorkspaceManager) syncPanedDivider(paned *gtk.Paned, orientation gtk.Orientation, guard *paneNode) { | |
| 1352 | + | if wm == nil || paned == nil { | |
| 1353 | + | return | |
| 1354 | + | } | |
| 1355 | + | ||
| 1356 | + | const ( | |
| 1357 | + | minDimension = 32 | |
| 1358 | + | maxFrames = 120 | |
| 1359 | + | ) | |
| 1360 | + | ||
| 1361 | + | setPosition := func() bool { | |
| 1362 | + | if guard != nil && !guard.widgetValid { | |
| 1363 | + | return true | |
| 1364 | + | } | |
| 1365 | + | ||
| 1366 | + | alloc := paned.Allocation() | |
| 1367 | + | dimension := alloc.Width() | |
| 1368 | + | if orientation == gtk.OrientationVertical { | |
| 1369 | + | dimension = alloc.Height() | |
| 1370 | + | } | |
| 1371 | + | ||
| 1372 | + | if dimension < minDimension { | |
| 1373 | + | return false | |
| 1374 | + | } | |
| 1375 | + | ||
| 1376 | + | pos := dimension / 2 | |
| 1377 | + | if pos <= 0 { | |
| 1378 | + | return false | |
| 1379 | + | } | |
| 1380 | + | ||
| 1381 | + | paned.SetPosition(pos) | |
| 1382 | + | log.Printf("[workspace] Set paned position to 50%%: %d (orientation=%d, size=%dx%d)", pos, orientation, alloc.Width(), alloc.Height()) | |
| 1383 | + | return true | |
| 1384 | + | } | |
| 1385 | + | ||
| 1386 | + | // Attempt immediately with the current allocation. | |
| 1387 | + | if setPosition() { | |
| 1388 | + | return | |
| 1389 | + | } | |
| 1390 | + | ||
| 1391 | + | var frames uint | |
| 1392 | + | ||
| 1393 | + | paned.ConnectMap(func() { | |
| 1394 | + | setPosition() | |
| 1395 | + | }) | |
| 1396 | + | ||
| 1397 | + | paned.AddTickCallback(func(widget gtk.Widgetter, _ gdk.FrameClocker) bool { | |
| 1398 | + | if guard != nil && !guard.widgetValid { | |
| 1399 | + | return false | |
| 1400 | + | } | |
| 1401 | + | ||
| 1402 | + | frames++ | |
| 1403 | + | if setPosition() { | |
| 1404 | + | return false | |
| 1405 | + | } | |
| 1406 | + | ||
| 1407 | + | if frames >= maxFrames { | |
| 1408 | + | alloc := paned.Allocation() | |
| 1409 | + | log.Printf("[workspace] Unable to stabilize paned position after %d frames (size=%dx%d)", frames, alloc.Width(), alloc.Height()) | |
| 1410 | + | return false | |
| 1411 | + | } | |
| 1412 | + | ||
| 1413 | + | return true | |
| 1414 | + | }) | |
| 1415 | + | ||
| 1416 | + | wm.scheduleIdleGuarded(func() bool { | |
| 1417 | + | setPosition() | |
| 1418 | + | return false | |
| 1419 | + | }, guard) | |
| 1420 | + | } | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "log" | 6 | "log" | ||
| 7 | 7 | ||||
| 8 | "github.com/bnema/dumber/pkg/webkit" | 8 | "github.com/bnema/dumber/pkg/webkit" | ||
| 9 | + | "github.com/diamondburned/gotk4/pkg/gdk/v4" | |||
| 9 | "github.com/diamondburned/gotk4/pkg/gtk/v4" | 10 | "github.com/diamondburned/gotk4/pkg/gtk/v4" | ||
| 10 | ) | 11 | ) | ||
| 11 | 12 | ||||
| @@ -497,27 +498,8 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 497 | return false | 498 | return false | ||
| 498 | }, split) | 499 | }, split) | ||
| 499 | 500 | ||||
| 500 | - | // CRITICAL: Set initial 50/50 split position after showing paned | 501 | + | // Synchronize the divider once the paned has a meaningful allocation. |
| 501 | - | // GTK needs the widget to be realized before we can set position based on allocation | 502 | + | wm.syncPanedDivider(paned, orientation, split) |
| 502 | - | // Schedule this to run after GTK has allocated space to the paned | |||
| 503 | - | wm.scheduleIdleGuarded(func() bool { | |||
| 504 | - | if paned == nil { | |||
| 505 | - | return false | |||
| 506 | - | } | |||
| 507 | - | // Get paned allocation to calculate 50% position | |||
| 508 | - | alloc := paned.Allocation() | |||
| 509 | - | var splitPos int | |||
| 510 | - | if orientation == gtk.OrientationHorizontal { | |||
| 511 | - | splitPos = alloc.Width() / 2 | |||
| 512 | - | } else { | |||
| 513 | - | splitPos = alloc.Height() / 2 | |||
| 514 | - | } | |||
| 515 | - | if splitPos > 0 { | |||
| 516 | - | paned.SetPosition(splitPos) | |||
| 517 | - | log.Printf("[workspace] Set initial paned position to 50%%: %d (orientation=%d, size=%dx%d)", splitPos, orientation, alloc.Width(), alloc.Height()) | |||
| 518 | - | } | |||
| 519 | - | return false | |||
| 520 | - | }, split) | |||
| 521 | 503 | ||||
| 522 | // CRITICAL: Show the split target container and force GTK to recreate rendering surface after reparenting | 504 | // CRITICAL: Show the split target container and force GTK to recreate rendering surface after reparenting | ||
| 523 | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | 505 | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | ||
| @@ -785,27 +767,8 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||||
| 785 | 767 | ||||
| 786 | paned.Show() | 768 | paned.Show() | ||
| 787 | 769 | ||||
| 788 | - | // CRITICAL: Set initial 50/50 split position after showing paned | 770 | + | // Synchronize the divider once the paned has a meaningful allocation. |
| 789 | - | // GTK needs the widget to be realized before we can set position based on allocation | 771 | + | wm.syncPanedDivider(paned, orientation, split) |
| 790 | - | // Schedule this to run after GTK has allocated space to the paned | |||
| 791 | - | wm.scheduleIdleGuarded(func() bool { | |||
| 792 | - | if paned == nil { | |||
| 793 | - | return false | |||
| 794 | - | } | |||
| 795 | - | // Get paned allocation to calculate 50% position | |||
| 796 | - | alloc := paned.Allocation() | |||
| 797 | - | var splitPos int | |||
| 798 | - | if orientation == gtk.OrientationHorizontal { | |||
| 799 | - | splitPos = alloc.Width() / 2 | |||
| 800 | - | } else { | |||
| 801 | - | splitPos = alloc.Height() / 2 | |||
| 802 | - | } | |||
| 803 | - | if splitPos > 0 { | |||
| 804 | - | paned.SetPosition(splitPos) | |||
| 805 | - | log.Printf("[workspace] Set initial paned position to 50%%: %d (orientation=%d, size=%dx%d)", splitPos, orientation, alloc.Width(), alloc.Height()) | |||
| 806 | - | } | |||
| 807 | - | return false | |||
| 808 | - | }, split) | |||
| 809 | 772 | ||||
| 810 | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | 773 | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | ||
| 811 | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | 774 | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | ||
| @@ -1381,3 +1344,77 @@ func (wm *WorkspaceManager) closeStackedPaneCompat(node *paneNode) (*paneNode, e | |||||
| 1381 | 1344 | ||||
| 1382 | return stack, nil | 1345 | return stack, nil | ||
| 1383 | } | 1346 | } | ||
| 1347 | + | ||||
| 1348 | + | // syncPanedDivider waits until a GtkPaned has a meaningful allocation before snapping | |||
| 1349 | + | // the divider to an even split. GTK often reports near-zero allocations for a few frames | |||
| 1350 | + | // after insertion, so we retry on map/tick/idle until the size stabilizes. | |||
| 1351 | + | func (wm *WorkspaceManager) syncPanedDivider(paned *gtk.Paned, orientation gtk.Orientation, guard *paneNode) { | |||
| 1352 | + | if wm == nil || paned == nil { | |||
| 1353 | + | return | |||
| 1354 | + | } | |||
| 1355 | + | ||||
| 1356 | + | const ( | |||
| 1357 | + | minDimension = 32 | |||
| 1358 | + | maxFrames = 120 | |||
| 1359 | + | ) | |||
| 1360 | + | ||||
| 1361 | + | setPosition := func() bool { | |||
| 1362 | + | if guard != nil && !guard.widgetValid { | |||
| 1363 | + | return true | |||
| 1364 | + | } | |||
| 1365 | + | ||||
| 1366 | + | alloc := paned.Allocation() | |||
| 1367 | + | dimension := alloc.Width() | |||
| 1368 | + | if orientation == gtk.OrientationVertical { | |||
| 1369 | + | dimension = alloc.Height() | |||
| 1370 | + | } | |||
| 1371 | + | ||||
| 1372 | + | if dimension < minDimension { | |||
| 1373 | + | return false | |||
| 1374 | + | } | |||
| 1375 | + | ||||
| 1376 | + | pos := dimension / 2 | |||
| 1377 | + | if pos <= 0 { | |||
| 1378 | + | return false | |||
| 1379 | + | } | |||
| 1380 | + | ||||
| 1381 | + | paned.SetPosition(pos) | |||
| 1382 | + | log.Printf("[workspace] Set paned position to 50%%: %d (orientation=%d, size=%dx%d)", pos, orientation, alloc.Width(), alloc.Height()) | |||
| 1383 | + | return true | |||
| 1384 | + | } | |||
| 1385 | + | ||||
| 1386 | + | // Attempt immediately with the current allocation. | |||
| 1387 | + | if setPosition() { | |||
| 1388 | + | return | |||
| 1389 | + | } | |||
| 1390 | + | ||||
| 1391 | + | var frames uint | |||
| 1392 | + | ||||
| 1393 | + | paned.ConnectMap(func() { | |||
| 1394 | + | setPosition() | |||
| 1395 | + | }) | |||
| 1396 | + | ||||
| 1397 | + | paned.AddTickCallback(func(widget gtk.Widgetter, _ gdk.FrameClocker) bool { | |||
| 1398 | + | if guard != nil && !guard.widgetValid { | |||
| 1399 | + | return false | |||
| 1400 | + | } | |||
| 1401 | + | ||||
| 1402 | + | frames++ | |||
| 1403 | + | if setPosition() { | |||
| 1404 | + | return false | |||
| 1405 | + | } | |||
| 1406 | + | ||||
| 1407 | + | if frames >= maxFrames { | |||
| 1408 | + | alloc := paned.Allocation() | |||
| 1409 | + | log.Printf("[workspace] Unable to stabilize paned position after %d frames (size=%dx%d)", frames, alloc.Width(), alloc.Height()) | |||
| 1410 | + | return false | |||
| 1411 | + | } | |||
| 1412 | + | ||||
| 1413 | + | return true | |||
| 1414 | + | }) | |||
| 1415 | + | ||||
| 1416 | + | wm.scheduleIdleGuarded(func() bool { | |||
| 1417 | + | setPosition() | |||
| 1418 | + | return false | |||
| 1419 | + | }, guard) | |||
| 1420 | + | } | |||
M internal/app/browser/workspace_popup.go +322 -551
| @@ -2,13 +2,14 @@ | |||
| 2 | 2 | package browser | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | - | "fmt" | |
| 6 | 5 | "log" | |
| 7 | 6 | "strings" | |
| 8 | 7 | "time" | |
| 9 | 8 | ||
| 10 | - | "github.com/bnema/dumber/internal/app/messaging" | |
| 9 | + | "github.com/bnema/dumber/internal/config" | |
| 11 | 10 | "github.com/bnema/dumber/pkg/webkit" | |
| 11 | + | webkitgtk "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | |
| 12 | + | "github.com/diamondburned/gotk4/pkg/glib/v2" | |
| 12 | 13 | ) | |
| 13 | 14 | ||
| 14 | 15 | const ( | |
| @@ -17,264 +18,9 @@ const ( | |||
| 17 | 18 | defaultPopupMinHeight = 600 // Minimum height for OAuth popups | |
| 18 | 19 | ) | |
| 19 | 20 | ||
| 20 | - | // HandlePopup handles popup window creation requests from WebViews | |
| 21 | - | func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *webkit.WebView { | |
| 22 | - | log.Printf("[workspace] HandlePopup called for URL: %s", url) | |
| 23 | - | ||
| 24 | - | // Check for frame type markers added by WebKit layer | |
| 25 | - | isBlankTarget := strings.HasSuffix(url, "#__dumber_frame_blank") | |
| 26 | - | isPopupTarget := strings.HasSuffix(url, "#__dumber_frame_popup") | |
| 27 | - | ||
| 28 | - | // Clean the URL by removing our markers | |
| 29 | - | if isBlankTarget || isPopupTarget { | |
| 30 | - | if isBlankTarget { | |
| 31 | - | url = strings.TrimSuffix(url, "#__dumber_frame_blank") | |
| 32 | - | log.Printf("[workspace] Detected _blank target - will create regular pane for: %s", url) | |
| 33 | - | } else { | |
| 34 | - | url = strings.TrimSuffix(url, "#__dumber_frame_popup") | |
| 35 | - | log.Printf("[workspace] Detected popup target - will create popup pane for: %s", url) | |
| 36 | - | } | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | if wm == nil || source == nil { | |
| 40 | - | log.Printf("[workspace] HandlePopup: nil workspace manager or source - allowing native popup") | |
| 41 | - | return nil | |
| 42 | - | } | |
| 43 | - | ||
| 44 | - | node := wm.viewToNode[source] | |
| 45 | - | if node == nil { | |
| 46 | - | log.Printf("[workspace] popup from unknown webview - allowing native popup") | |
| 47 | - | return nil | |
| 48 | - | } | |
| 49 | - | ||
| 50 | - | // Note: HandlePopup is now obsolete - window.open is handled directly via JavaScript bypass | |
| 51 | - | ||
| 52 | - | cfg := wm.app.config | |
| 53 | - | if cfg == nil { | |
| 54 | - | log.Printf("[workspace] HandlePopup: nil config - allowing native popup") | |
| 55 | - | return nil | |
| 56 | - | } | |
| 57 | - | ||
| 58 | - | popCfg := cfg.Workspace.Popups | |
| 59 | - | log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement) | |
| 60 | - | ||
| 61 | - | if !popCfg.OpenInNewPane { | |
| 62 | - | log.Printf("[workspace] Popup creation disabled in config - allowing native popup") | |
| 63 | - | return nil | |
| 64 | - | } | |
| 65 | - | ||
| 66 | - | // Smart detection path: create temporary view and decide placement once type is known | |
| 67 | - | if popCfg.EnableSmartDetection { | |
| 68 | - | webkitCfg, err := wm.app.buildWebkitConfig() | |
| 69 | - | if err != nil { | |
| 70 | - | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |
| 71 | - | return nil | |
| 72 | - | } | |
| 73 | - | // Popups are embedded in workspace paned containers - no separate window needed | |
| 74 | - | webkitCfg.CreateWindow = false | |
| 75 | - | // Create as related to avoid WindowFeatures crash; we'll decide final placement later | |
| 76 | - | newView, err := webkit.NewWebViewWithRelated(webkitCfg, source) | |
| 77 | - | if err != nil { | |
| 78 | - | log.Printf("[workspace] failed to create temp WebView: %v - allowing native popup", err) | |
| 79 | - | return nil | |
| 80 | - | } | |
| 81 | - | ||
| 82 | - | // Register detection callback | |
| 83 | - | newView.OnWindowTypeDetected(func(t webkit.WindowType, feat *webkit.WindowFeatures) { | |
| 84 | - | wm.RunOnUI(func() { | |
| 85 | - | wm.handleDetectedWindowType(node, newView, url, t, feat) | |
| 86 | - | }) | |
| 87 | - | }) | |
| 88 | - | ||
| 89 | - | // Fallback: if detection never fires, treat as popup as before | |
| 90 | - | go func() { | |
| 91 | - | time.Sleep(1500 * time.Millisecond) | |
| 92 | - | if newView != nil { | |
| 93 | - | wm.RunOnUI(func() { | |
| 94 | - | if wm.viewToNode[newView] == nil { // not yet placed | |
| 95 | - | wm.handleDetectedWindowType(node, newView, url, webkit.WindowTypePopup, nil) | |
| 96 | - | } | |
| 97 | - | }) | |
| 98 | - | } | |
| 99 | - | }() | |
| 100 | - | ||
| 101 | - | return newView | |
| 102 | - | } | |
| 103 | - | ||
| 104 | - | // Legacy path preserved | |
| 105 | - | webkitCfg, err := wm.app.buildWebkitConfig() | |
| 106 | - | if err != nil { | |
| 107 | - | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |
| 108 | - | return nil | |
| 109 | - | } | |
| 110 | - | // Popups are embedded in workspace paned containers - no separate window needed | |
| 111 | - | webkitCfg.CreateWindow = false | |
| 112 | - | newView, err := webkit.NewWebViewWithRelated(webkitCfg, source) | |
| 113 | - | if err != nil { | |
| 114 | - | log.Printf("[workspace] failed to create placeholder WebView: %v - allowing native popup", err) | |
| 115 | - | return nil | |
| 116 | - | } | |
| 117 | - | ||
| 118 | - | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler | |
| 119 | - | ||
| 120 | - | // Create a pane for the new WebView | |
| 121 | - | newPane, err := wm.createPane(newView) | |
| 122 | - | if err != nil { | |
| 123 | - | log.Printf("[workspace] failed to create popup pane: %v - allowing native popup", err) | |
| 124 | - | return nil | |
| 125 | - | } | |
| 126 | - | ||
| 127 | - | // Determine placement direction | |
| 128 | - | direction := strings.ToLower(popCfg.Placement) | |
| 129 | - | if direction == "" { | |
| 130 | - | direction = "right" | |
| 131 | - | } | |
| 132 | - | ||
| 133 | - | // Determine target node for splitting | |
| 134 | - | target := node | |
| 135 | - | if !popCfg.FollowPaneContext && wm.GetActiveNode() != nil { | |
| 136 | - | target = wm.GetActiveNode() | |
| 137 | - | } | |
| 138 | - | ||
| 139 | - | // Add the popup pane to the workspace using manual pane insertion | |
| 140 | - | if err := wm.insertPopupPane(target, newPane, direction); err != nil { | |
| 141 | - | log.Printf("[workspace] popup pane insertion failed: %v - allowing native popup", err) | |
| 142 | - | return nil | |
| 143 | - | } | |
| 144 | - | ||
| 145 | - | // Get the new node and popup WebView ID for tracking | |
| 146 | - | newNode := wm.viewToNode[newView] | |
| 147 | - | popupWebViewID := newView.IDString() | |
| 148 | - | ||
| 149 | - | // Apply different behavior based on target type | |
| 150 | - | if isBlankTarget { | |
| 151 | - | log.Printf("[workspace] Treating _blank target as regular pane - no auto-close behavior") | |
| 152 | - | // For _blank targets, set up parent relationship but no auto-close | |
| 153 | - | if newNode != nil { | |
| 154 | - | newNode.parentPane = node | |
| 155 | - | } | |
| 156 | - | ||
| 157 | - | // Register close handler to clean up activePopupChildren tracking | |
| 158 | - | newView.RegisterCloseHandler(func() { | |
| 159 | - | log.Printf("[workspace] _blank popup requesting close via window.close()") | |
| 160 | - | // Remove this popup from parent's activePopupChildren | |
| 161 | - | if node != nil && popupWebViewID != "" { | |
| 162 | - | for i, childID := range node.activePopupChildren { | |
| 163 | - | if childID == popupWebViewID { | |
| 164 | - | node.activePopupChildren = append(node.activePopupChildren[:i], node.activePopupChildren[i+1:]...) | |
| 165 | - | log.Printf("[workspace] Removed _blank popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(node.activePopupChildren)) | |
| 166 | - | break | |
| 167 | - | } | |
| 168 | - | } | |
| 169 | - | } | |
| 170 | - | }) | |
| 171 | - | } else { | |
| 172 | - | log.Printf("[workspace] Treating as popup pane - applying popup-specific behavior") | |
| 173 | - | // Mark as popup for auto-close handling (OAuth flows, etc.) | |
| 174 | - | if newNode != nil { | |
| 175 | - | newNode.isPopup = true | |
| 176 | - | newNode.parentPane = node | |
| 177 | - | log.Printf("[workspace] Marked pane as popup for auto-close handling") | |
| 178 | - | ||
| 179 | - | // Register close handler for popup auto-close on window.close() | |
| 180 | - | newView.RegisterCloseHandler(func() { | |
| 181 | - | log.Printf("[workspace] Popup requesting close via window.close()") | |
| 182 | - | ||
| 183 | - | // Remove this popup from parent's activePopupChildren | |
| 184 | - | if node != nil && popupWebViewID != "" { | |
| 185 | - | for i, childID := range node.activePopupChildren { | |
| 186 | - | if childID == popupWebViewID { | |
| 187 | - | node.activePopupChildren = append(node.activePopupChildren[:i], node.activePopupChildren[i+1:]...) | |
| 188 | - | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(node.activePopupChildren)) | |
| 189 | - | break | |
| 190 | - | } | |
| 191 | - | } | |
| 192 | - | } | |
| 193 | - | ||
| 194 | - | // Look up the node at close time | |
| 195 | - | if node := wm.viewToNode[newView]; node != nil && node.isPopup { | |
| 196 | - | log.Printf("[workspace] Closing popup pane") | |
| 197 | - | // Brief delay to allow any final redirects to complete | |
| 198 | - | time.AfterFunc(200*time.Millisecond, func() { | |
| 199 | - | wm.scheduleIdleGuarded(func() bool { | |
| 200 | - | if node == nil || !node.widgetValid { | |
| 201 | - | return false | |
| 202 | - | } | |
| 203 | - | if err := wm.ClosePane(node); err != nil { | |
| 204 | - | log.Printf("[workspace] Failed to close popup pane: %v", err) | |
| 205 | - | } | |
| 206 | - | return false | |
| 207 | - | }, node) | |
| 208 | - | }) | |
| 209 | - | } else { | |
| 210 | - | log.Printf("[workspace] Could not find popup node for close handler") | |
| 211 | - | } | |
| 212 | - | }) | |
| 213 | - | } else { | |
| 214 | - | log.Printf("[workspace] Warning: could not find node for popup WebView in viewToNode map") | |
| 215 | - | } | |
| 216 | - | } | |
| 217 | - | ||
| 218 | - | // Track this popup in the parent's activePopupChildren (for both _blank and popup types) | |
| 219 | - | // This is critical to prevent OAuth flows from hijacking the parent pane via window.opener | |
| 220 | - | if node != nil && popupWebViewID != "" { | |
| 221 | - | if node.activePopupChildren == nil { | |
| 222 | - | node.activePopupChildren = make([]string, 0) | |
| 223 | - | } | |
| 224 | - | node.activePopupChildren = append(node.activePopupChildren, popupWebViewID) | |
| 225 | - | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(node.activePopupChildren)) | |
| 226 | - | ||
| 227 | - | // Register navigation policy handler for parent to block script-initiated navigations | |
| 228 | - | // when it has active popup children (prevents OAuth from hijacking parent pane) | |
| 229 | - | if node.pane != nil && node.pane.WebView() != nil { | |
| 230 | - | node.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |
| 231 | - | // Always allow user-initiated navigations | |
| 232 | - | if isUserGesture { | |
| 233 | - | return true | |
| 234 | - | } | |
| 235 | - | ||
| 236 | - | // Block script-initiated navigations if this pane has active popup children | |
| 237 | - | if len(node.activePopupChildren) > 0 { | |
| 238 | - | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(node.activePopupChildren), url) | |
| 239 | - | return false | |
| 240 | - | } | |
| 241 | - | ||
| 242 | - | // Allow if no active popups | |
| 243 | - | return true | |
| 244 | - | }) | |
| 245 | - | log.Printf("[workspace] Registered navigation policy handler for parent pane") | |
| 246 | - | } | |
| 247 | - | } | |
| 248 | - | ||
| 249 | - | // Ensure GUI components are available in the new pane | |
| 250 | - | wm.ensureGUIInPane(newPane) | |
| 251 | - | ||
| 252 | - | // Inject GUI components into the popup pane | |
| 253 | - | wm.ensureGUIInPane(newPane) | |
| 254 | - | ||
| 255 | - | // Load the URL if provided | |
| 256 | - | if url != "" { | |
| 257 | - | paneType := "popup" | |
| 258 | - | if isBlankTarget { | |
| 259 | - | paneType = "_blank target" | |
| 260 | - | } | |
| 261 | - | log.Printf("[webkit] LoadURL (%s): %s", paneType, url) | |
| 262 | - | if err := newView.LoadURL(url); err != nil { | |
| 263 | - | log.Printf("[workspace] failed to load %s URL: %v", paneType, err) | |
| 264 | - | } | |
| 265 | - | // Ensure the WebView is visible after loading URL | |
| 266 | - | if err := newView.Show(); err != nil { | |
| 267 | - | log.Printf("[workspace] failed to show popup WebView: %v", err) | |
| 268 | - | } | |
| 269 | - | } | |
| 270 | - | ||
| 271 | - | if isBlankTarget { | |
| 272 | - | log.Printf("[workspace] Created regular pane for _blank target URL: %s", url) | |
| 273 | - | } else { | |
| 274 | - | log.Printf("[workspace] Created popup pane for URL: %s", url) | |
| 275 | - | } | |
| 276 | - | return newView | |
| 277 | - | } | |
| 21 | + | // HandlePopup is DEPRECATED and removed. | |
| 22 | + | // Popup lifecycle now uses WebKit's native create/ready-to-show/close signals. | |
| 23 | + | // See setupPopupHandling(), handlePopupReadyToShow(), and handlePopupClose() instead. | |
| 278 | 24 | ||
| 279 | 25 | // registerOAuthAutoClose sets up OAuth auto-close functionality for popups | |
| 280 | 26 | // Note: OAuth detection is now handled by the main-world.js injection script | |
| @@ -283,296 +29,6 @@ func (wm *WorkspaceManager) registerOAuthAutoClose(view *webkit.WebView, url str | |||
| 283 | 29 | log.Printf("[workspace] OAuth detection will be handled by main-world.js injection script") | |
| 284 | 30 | } | |
| 285 | 31 | ||
| 286 | - | // applyWindowFeatures applies window features to a WebView based on intent | |
| 287 | - | func (wm *WorkspaceManager) applyWindowFeatures(view *webkit.WebView, intent *messaging.WindowIntent, isPopup bool) { | |
| 288 | - | if intent == nil { | |
| 289 | - | return | |
| 290 | - | } | |
| 291 | - | ||
| 292 | - | features := &webkit.WindowFeatures{} | |
| 293 | - | ||
| 294 | - | // Apply dimensions if specified | |
| 295 | - | if intent.Width != nil { | |
| 296 | - | features.Width = *intent.Width | |
| 297 | - | } | |
| 298 | - | if intent.Height != nil { | |
| 299 | - | features.Height = *intent.Height | |
| 300 | - | } | |
| 301 | - | ||
| 302 | - | // Apply visibility features based on window type | |
| 303 | - | defaultToolbar := !isPopup | |
| 304 | - | defaultLocation := !isPopup | |
| 305 | - | defaultMenubar := !isPopup | |
| 306 | - | ||
| 307 | - | if intent.Toolbar != nil { | |
| 308 | - | features.ToolbarVisible = *intent.Toolbar | |
| 309 | - | } else { | |
| 310 | - | features.ToolbarVisible = defaultToolbar | |
| 311 | - | } | |
| 312 | - | ||
| 313 | - | if intent.Location != nil { | |
| 314 | - | features.LocationbarVisible = *intent.Location | |
| 315 | - | } else { | |
| 316 | - | features.LocationbarVisible = defaultLocation | |
| 317 | - | } | |
| 318 | - | ||
| 319 | - | if intent.Menubar != nil { | |
| 320 | - | features.MenubarVisible = *intent.Menubar | |
| 321 | - | } else { | |
| 322 | - | features.MenubarVisible = defaultMenubar | |
| 323 | - | } | |
| 324 | - | ||
| 325 | - | if intent.Resizable != nil { | |
| 326 | - | features.Resizable = *intent.Resizable | |
| 327 | - | } else { | |
| 328 | - | features.Resizable = true // Usually resizable unless explicitly disabled | |
| 329 | - | } | |
| 330 | - | ||
| 331 | - | view.SetWindowFeatures(features) | |
| 332 | - | windowTypeStr := "tab" | |
| 333 | - | if isPopup { | |
| 334 | - | windowTypeStr = "popup" | |
| 335 | - | } | |
| 336 | - | log.Printf("[workspace] Applied %s window features from intent: size=%dx%d, toolbar=%t, location=%t, menubar=%t, resizable=%t", | |
| 337 | - | windowTypeStr, features.Width, features.Height, features.ToolbarVisible, features.LocationbarVisible, features.MenubarVisible, features.Resizable) | |
| 338 | - | } | |
| 339 | - | ||
| 340 | - | // handleIntentAsTab creates a tab pane based on window.open intent | |
| 341 | - | func (wm *WorkspaceManager) handleIntentAsTab(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView { | |
| 342 | - | log.Printf("[workspace] Handling intent as tab: %s", url) | |
| 343 | - | ||
| 344 | - | webkitCfg, err := wm.app.buildWebkitConfig() | |
| 345 | - | if err != nil { | |
| 346 | - | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |
| 347 | - | return nil | |
| 348 | - | } | |
| 349 | - | // Tabs are embedded in workspace paned containers - no separate window needed | |
| 350 | - | webkitCfg.CreateWindow = false | |
| 351 | - | ||
| 352 | - | newView, err := webkit.NewWebView(webkitCfg) | |
| 353 | - | if err != nil { | |
| 354 | - | log.Printf("[workspace] failed to create tab WebView: %v - allowing native popup", err) | |
| 355 | - | return nil | |
| 356 | - | } | |
| 357 | - | ||
| 358 | - | newPane, err := wm.createPane(newView) | |
| 359 | - | if err != nil { | |
| 360 | - | log.Printf("[workspace] failed to create tab pane: %v - allowing native popup", err) | |
| 361 | - | return nil | |
| 362 | - | } | |
| 363 | - | ||
| 364 | - | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 365 | - | if direction == "" { | |
| 366 | - | direction = "right" | |
| 367 | - | } | |
| 368 | - | ||
| 369 | - | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { | |
| 370 | - | log.Printf("[workspace] tab pane insertion failed: %v - allowing native popup", err) | |
| 371 | - | return nil | |
| 372 | - | } | |
| 373 | - | ||
| 374 | - | node := wm.viewToNode[newView] | |
| 375 | - | if node != nil { | |
| 376 | - | node.windowType = webkit.WindowTypeTab | |
| 377 | - | node.isRelated = false | |
| 378 | - | ||
| 379 | - | // Apply window features from JavaScript intent | |
| 380 | - | wm.applyWindowFeatures(newView, intent, false) | |
| 381 | - | } | |
| 382 | - | ||
| 383 | - | wm.ensureGUIInPane(newPane) | |
| 384 | - | ||
| 385 | - | if url != "" { | |
| 386 | - | if err := newView.LoadURL(url); err != nil { | |
| 387 | - | log.Printf("[workspace] failed to load tab URL: %v", err) | |
| 388 | - | } | |
| 389 | - | if err := newView.Show(); err != nil { | |
| 390 | - | log.Printf("[workspace] failed to show tab WebView: %v", err) | |
| 391 | - | } | |
| 392 | - | } | |
| 393 | - | ||
| 394 | - | log.Printf("[workspace] Created tab pane for URL: %s", url) | |
| 395 | - | return newView | |
| 396 | - | } | |
| 397 | - | ||
| 398 | - | // handleIntentAsPopup creates a related popup pane based on window.open intent | |
| 399 | - | func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView { | |
| 400 | - | log.Printf("[workspace] Handling intent as popup: %s", url) | |
| 401 | - | ||
| 402 | - | webkitCfg, err := wm.app.buildWebkitConfig() | |
| 403 | - | if err != nil { | |
| 404 | - | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |
| 405 | - | return nil | |
| 406 | - | } | |
| 407 | - | // Popups are embedded in workspace paned containers - no separate window needed | |
| 408 | - | webkitCfg.CreateWindow = false | |
| 409 | - | ||
| 410 | - | newView, err := webkit.NewWebViewWithRelated(webkitCfg, sourceNode.pane.webView) | |
| 411 | - | if err != nil { | |
| 412 | - | log.Printf("[workspace] failed to create popup WebView: %v - allowing native popup", err) | |
| 413 | - | return nil | |
| 414 | - | } | |
| 415 | - | ||
| 416 | - | // Log the parent-popup WebView ID relationship for OAuth auto-close | |
| 417 | - | parentWebViewID := sourceNode.pane.webView.IDString() | |
| 418 | - | popupWebViewID := newView.IDString() | |
| 419 | - | log.Printf("[workspace] Created popup WebView: parentID=%s popupID=%s url=%s", parentWebViewID, popupWebViewID, url) | |
| 420 | - | ||
| 421 | - | // Store popup WebView ID in parent's localStorage for OAuth callback lookup | |
| 422 | - | storeScript := fmt.Sprintf(` | |
| 423 | - | try { | |
| 424 | - | const parentWebViewId = '%s'; | |
| 425 | - | const popupWebViewId = '%s'; | |
| 426 | - | const popupMapping = { | |
| 427 | - | parentId: parentWebViewId, | |
| 428 | - | popupId: popupWebViewId, | |
| 429 | - | timestamp: Date.now(), | |
| 430 | - | url: '%s' | |
| 431 | - | }; | |
| 432 | - | localStorage.setItem('popup_mapping_' + parentWebViewId, JSON.stringify(popupMapping)); | |
| 433 | - | console.log('[workspace] Stored popup mapping:', popupMapping); | |
| 434 | - | } catch(e) { | |
| 435 | - | console.warn('[workspace] Failed to store popup mapping:', e); | |
| 436 | - | } | |
| 437 | - | `, parentWebViewID, popupWebViewID, url) | |
| 438 | - | ||
| 439 | - | // Inject into parent WebView so it can find its popup later | |
| 440 | - | if err := sourceNode.pane.webView.InjectScript(storeScript); err != nil { | |
| 441 | - | log.Printf("[workspace] Failed to inject popup mapping script into parent: %v", err) | |
| 442 | - | } | |
| 443 | - | ||
| 444 | - | newPane, err := wm.createPane(newView) | |
| 445 | - | if err != nil { | |
| 446 | - | log.Printf("[workspace] failed to create popup pane: %v - allowing native popup", err) | |
| 447 | - | return nil | |
| 448 | - | } | |
| 449 | - | ||
| 450 | - | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 451 | - | if direction == "" { | |
| 452 | - | direction = "right" | |
| 453 | - | } | |
| 454 | - | ||
| 455 | - | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { | |
| 456 | - | log.Printf("[workspace] popup pane insertion failed: %v - allowing native popup", err) | |
| 457 | - | return nil | |
| 458 | - | } | |
| 459 | - | ||
| 460 | - | node := wm.viewToNode[newView] | |
| 461 | - | var requestID string | |
| 462 | - | if node != nil { | |
| 463 | - | node.windowType = webkit.WindowTypePopup | |
| 464 | - | node.isRelated = true | |
| 465 | - | node.parentPane = sourceNode | |
| 466 | - | node.isPopup = true | |
| 467 | - | node.autoClose = wm.shouldAutoClose(url) | |
| 468 | - | ||
| 469 | - | // Track this popup in the parent's activePopupChildren | |
| 470 | - | if sourceNode != nil { | |
| 471 | - | if sourceNode.activePopupChildren == nil { | |
| 472 | - | sourceNode.activePopupChildren = make([]string, 0) | |
| 473 | - | } | |
| 474 | - | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren, popupWebViewID) | |
| 475 | - | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |
| 476 | - | ||
| 477 | - | // Register navigation policy handler for parent to block script-initiated navigations | |
| 478 | - | // when it has active popup children (prevents OAuth from hijacking parent pane) | |
| 479 | - | if sourceNode.pane != nil && sourceNode.pane.WebView() != nil { | |
| 480 | - | sourceNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |
| 481 | - | // Always allow user-initiated navigations | |
| 482 | - | if isUserGesture { | |
| 483 | - | return true | |
| 484 | - | } | |
| 485 | - | ||
| 486 | - | // Block script-initiated navigations if this pane has active popup children | |
| 487 | - | if len(sourceNode.activePopupChildren) > 0 { | |
| 488 | - | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url) | |
| 489 | - | return false | |
| 490 | - | } | |
| 491 | - | ||
| 492 | - | // Allow if no active popups | |
| 493 | - | return true | |
| 494 | - | }) | |
| 495 | - | log.Printf("[workspace] Registered navigation policy handler for parent pane") | |
| 496 | - | } | |
| 497 | - | } | |
| 498 | - | ||
| 499 | - | // Store requestID for deduplication cleanup | |
| 500 | - | if intent != nil { | |
| 501 | - | requestID = intent.RequestID | |
| 502 | - | node.requestID = requestID | |
| 503 | - | } | |
| 504 | - | ||
| 505 | - | // Apply window features from JavaScript intent | |
| 506 | - | wm.applyWindowFeatures(newView, intent, true) | |
| 507 | - | ||
| 508 | - | // Apply minimum size constraints to prevent compression | |
| 509 | - | var width, height int | |
| 510 | - | if intent != nil { | |
| 511 | - | if intent.Width != nil { | |
| 512 | - | width = *intent.Width | |
| 513 | - | } | |
| 514 | - | if intent.Height != nil { | |
| 515 | - | height = *intent.Height | |
| 516 | - | } | |
| 517 | - | } | |
| 518 | - | wm.applyPopupSizeConstraints(newView, width, height) | |
| 519 | - | } | |
| 520 | - | ||
| 521 | - | // Register close handler for popup auto-close | |
| 522 | - | newView.RegisterCloseHandler(func() { | |
| 523 | - | log.Printf("[workspace] Popup requesting close via window.close()") | |
| 524 | - | ||
| 525 | - | // Clear the RequestID from deduplicator to allow new popups with same ID | |
| 526 | - | if requestID != "" && wm.paneDeduplicator != nil { | |
| 527 | - | wm.paneDeduplicator.ClearRequestID(requestID) | |
| 528 | - | } | |
| 529 | - | ||
| 530 | - | // Remove this popup from parent's activePopupChildren | |
| 531 | - | if sourceNode != nil && popupWebViewID != "" { | |
| 532 | - | for i, childID := range sourceNode.activePopupChildren { | |
| 533 | - | if childID == popupWebViewID { | |
| 534 | - | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren[:i], sourceNode.activePopupChildren[i+1:]...) | |
| 535 | - | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |
| 536 | - | break | |
| 537 | - | } | |
| 538 | - | } | |
| 539 | - | } | |
| 540 | - | ||
| 541 | - | if n := wm.viewToNode[newView]; n != nil && n.isPopup { | |
| 542 | - | time.AfterFunc(200*time.Millisecond, func() { | |
| 543 | - | wm.scheduleIdleGuarded(func() bool { | |
| 544 | - | if n == nil || !n.widgetValid { | |
| 545 | - | return false | |
| 546 | - | } | |
| 547 | - | if err := wm.ClosePane(n); err != nil { | |
| 548 | - | log.Printf("[workspace] Failed to close popup pane: %v", err) | |
| 549 | - | } | |
| 550 | - | return false | |
| 551 | - | }, n) | |
| 552 | - | }) | |
| 553 | - | } | |
| 554 | - | }) | |
| 555 | - | ||
| 556 | - | // URL-based auto-close for OAuth popups | |
| 557 | - | if node != nil && node.isPopup && node.autoClose { | |
| 558 | - | wm.registerOAuthAutoClose(newView, url) | |
| 559 | - | } | |
| 560 | - | ||
| 561 | - | wm.ensureGUIInPane(newPane) | |
| 562 | - | ||
| 563 | - | if url != "" { | |
| 564 | - | if err := newView.LoadURL(url); err != nil { | |
| 565 | - | log.Printf("[workspace] failed to load popup URL: %v", err) | |
| 566 | - | } | |
| 567 | - | if err := newView.Show(); err != nil { | |
| 568 | - | log.Printf("[workspace] failed to show popup WebView: %v", err) | |
| 569 | - | } | |
| 570 | - | } | |
| 571 | - | ||
| 572 | - | log.Printf("[workspace] Created popup pane for URL: %s", url) | |
| 573 | - | return newView | |
| 574 | - | } | |
| 575 | - | ||
| 576 | 32 | // insertIndependentPane inserts a new independent pane next to the source | |
| 577 | 33 | func (wm *WorkspaceManager) insertIndependentPane(sourceNode *paneNode, webView *webkit.WebView, url string) error { | |
| 578 | 34 | newPane, err := wm.createPane(webView) | |
| @@ -803,7 +259,7 @@ func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webVi | |||
| 803 | 259 | // Tabs are embedded in workspace paned containers - no separate window needed | |
| 804 | 260 | webkitCfg.CreateWindow = false | |
| 805 | 261 | ||
| 806 | - | // Create independent WebView like handleIntentAsTab does | |
| 262 | + | // Create independent WebView matching our standard tab insertion path | |
| 807 | 263 | independentView, err := webkit.NewWebView(webkitCfg) | |
| 808 | 264 | if err != nil { | |
| 809 | 265 | log.Printf("[workspace] failed to create independent tab WebView: %v", err) | |
| @@ -824,3 +280,318 @@ func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webVi | |||
| 824 | 280 | wm.configureRelatedPopup(sourceNode, webView, url, features) | |
| 825 | 281 | } | |
| 826 | 282 | } | |
| 283 | + | ||
| 284 | + | // setupPopupHandling connects WebView's create signal for popup lifecycle management | |
| 285 | + | // This implements the WebKit-native popup lifecycle as described in POPUP_REFACTORING_PLAN.md | |
| 286 | + | func (wm *WorkspaceManager) setupPopupHandling(webView *webkit.WebView, parentNode *paneNode) { | |
| 287 | + | if webView == nil || parentNode == nil { | |
| 288 | + | return | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | webView.RegisterPopupCreateHandler(func(navAction *webkitgtk.NavigationAction) *webkit.WebView { | |
| 292 | + | // Get URL from navigation action | |
| 293 | + | req := navAction.Request() | |
| 294 | + | if req == nil { | |
| 295 | + | log.Printf("[workspace] create signal: no request in navigation action") | |
| 296 | + | return nil | |
| 297 | + | } | |
| 298 | + | url := req.URI() | |
| 299 | + | ||
| 300 | + | log.Printf("[workspace] create signal received for URL: %s", url) | |
| 301 | + | ||
| 302 | + | // Check if popups are enabled in config | |
| 303 | + | if wm.app.config != nil && !wm.app.config.Workspace.Popups.OpenInNewPane { | |
| 304 | + | log.Printf("[workspace] Popup creation disabled in config - blocking popup") | |
| 305 | + | return nil | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | // CRITICAL: Create a BARE gotk4 WebView as RELATED to parent (no wrapping, no initialization) | |
| 309 | + | // WebKit requires the related-view property to be set during construction for proper lifecycle | |
| 310 | + | // This ensures session/cookie sharing and proper WindowFeatures initialization | |
| 311 | + | // All initialization (wrapping, container, scripts) happens in ready-to-show | |
| 312 | + | log.Printf("[workspace] Step 1: Creating bare gotk4 WebView with related-view property") | |
| 313 | + | barePopupView := webkit.NewBareRelatedWebView(webView.GtkWebView()) | |
| 314 | + | if barePopupView == nil { | |
| 315 | + | log.Printf("[workspace] Failed to create bare related popup WebView - blocking popup") | |
| 316 | + | return nil | |
| 317 | + | } | |
| 318 | + | log.Printf("[workspace] Step 1 complete: bare related gotk4 WebView created=%p (parent=%p)", barePopupView, webView.GtkWebView()) | |
| 319 | + | ||
| 320 | + | // Wrap the bare view immediately to get a proper ID | |
| 321 | + | // BUT don't initialize it yet - that happens in ready-to-show | |
| 322 | + | log.Printf("[workspace] Step 2: Wrapping bare view (no initialization)") | |
| 323 | + | wrappedView := webkit.WrapBareWebView(barePopupView) | |
| 324 | + | if wrappedView == nil { | |
| 325 | + | log.Printf("[workspace] Failed to wrap bare popup WebView - blocking popup") | |
| 326 | + | return nil | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | // Use the wrapper's ID for tracking | |
| 330 | + | popupID := wrappedView.ID() | |
| 331 | + | log.Printf("[workspace] Step 2 complete: wrapped view created with ID=%d", popupID) | |
| 332 | + | ||
| 333 | + | // Store wrapped view (not initialized) for initialization in ready-to-show | |
| 334 | + | log.Printf("[workspace] Step 3: Storing in pendingPopups map") | |
| 335 | + | wm.pendingPopups[popupID] = &pendingPopup{ | |
| 336 | + | wrappedView: wrappedView, | |
| 337 | + | parentView: webView, | |
| 338 | + | parentNode: parentNode, | |
| 339 | + | url: url, | |
| 340 | + | } | |
| 341 | + | log.Printf("[workspace] Step 3 complete: stored in pendingPopups") | |
| 342 | + | ||
| 343 | + | // CRITICAL: Do NOT connect signals before returning to WebKit | |
| 344 | + | // Connecting signals might trigger WebKit to access WindowFeatures before it's set | |
| 345 | + | // Instead, we'll connect them after WebKit has processed the return | |
| 346 | + | log.Printf("[workspace] Step 4: Scheduling signal connections for next main loop iteration (AFTER WebKit configures the view)") | |
| 347 | + | ||
| 348 | + | // Use GLib idle_add to connect signals AFTER this callback returns | |
| 349 | + | // This ensures WebKit has finished configuring WindowFeatures before we touch the view | |
| 350 | + | glib.IdleAdd(func() bool { | |
| 351 | + | log.Printf("[workspace] Idle callback: Now connecting signals for popup ID=%d", popupID) | |
| 352 | + | ||
| 353 | + | // Connect ready-to-show signal NOW (after WebKit has configured the view) | |
| 354 | + | barePopupView.ConnectReadyToShow(func() { | |
| 355 | + | log.Printf("[workspace] ready-to-show signal received for popup ID=%d", popupID) | |
| 356 | + | wm.handlePopupReadyToShow(popupID) | |
| 357 | + | }) | |
| 358 | + | log.Printf("[workspace] Idle callback: ready-to-show connected") | |
| 359 | + | ||
| 360 | + | // Connect close handler | |
| 361 | + | barePopupView.ConnectClose(func() { | |
| 362 | + | log.Printf("[workspace] close signal received for popup ID=%d", popupID) | |
| 363 | + | wm.handlePopupClose(popupID) | |
| 364 | + | }) | |
| 365 | + | log.Printf("[workspace] Idle callback: close signal connected") | |
| 366 | + | ||
| 367 | + | return false // Don't repeat | |
| 368 | + | }) | |
| 369 | + | ||
| 370 | + | log.Printf("[workspace] Step 4 complete: signal connections scheduled") | |
| 371 | + | ||
| 372 | + | log.Printf("[workspace] Step 5: Returning wrapped view to WebKit (will extract bare view in ConnectCreate)") | |
| 373 | + | ||
| 374 | + | // Return the wrapped view - WebKit will extract the underlying gotk4 view and configure it | |
| 375 | + | return wrappedView | |
| 376 | + | }) | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | // handlePopupReadyToShow inserts popup pane when WebKit says it's ready | |
| 380 | + | func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) { | |
| 381 | + | info, ok := wm.pendingPopups[popupID] | |
| 382 | + | if !ok { | |
| 383 | + | log.Printf("[workspace] ready-to-show for unknown popup ID: %d", popupID) | |
| 384 | + | return | |
| 385 | + | } | |
| 386 | + | delete(wm.pendingPopups, popupID) | |
| 387 | + | ||
| 388 | + | log.Printf("[workspace] ready-to-show received for popup ID=%d, starting full initialization", popupID) | |
| 389 | + | ||
| 390 | + | // NOW it's safe to initialize (WebKit has configured WindowProperties) | |
| 391 | + | // We already have the wrapped view from setupPopupHandling | |
| 392 | + | wrappedView := info.wrappedView | |
| 393 | + | ||
| 394 | + | // Build webkit config for initialization | |
| 395 | + | webkitCfg, err := wm.app.buildWebkitConfig() | |
| 396 | + | if err != nil { | |
| 397 | + | log.Printf("[workspace] Failed to build webkit config for popup initialization: %v", err) | |
| 398 | + | return | |
| 399 | + | } | |
| 400 | + | webkitCfg.CreateWindow = false // Embedded in workspace | |
| 401 | + | ||
| 402 | + | // Initialize the wrapped view with full configuration | |
| 403 | + | if err := wrappedView.InitializeFromBare(webkitCfg); err != nil { | |
| 404 | + | log.Printf("[workspace] Failed to initialize popup WebView: %v", err) | |
| 405 | + | return | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | log.Printf("[workspace] Successfully initialized popup WebView ID %d", wrappedView.ID()) | |
| 409 | + | ||
| 410 | + | // Create pane for the fully initialized WebView | |
| 411 | + | popupPane, err := wm.createPane(wrappedView) | |
| 412 | + | if err != nil { | |
| 413 | + | log.Printf("[workspace] Failed to create popup pane: %v", err) | |
| 414 | + | return | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | log.Printf("[workspace] Created popup pane, now inserting into workspace") | |
| 418 | + | ||
| 419 | + | // NOW it's safe to insert into workspace based on configured behavior | |
| 420 | + | behavior := wm.app.config.Workspace.Popups.Behavior | |
| 421 | + | log.Printf("[workspace] Popup behavior: %s", behavior) | |
| 422 | + | ||
| 423 | + | switch behavior { | |
| 424 | + | case config.PopupBehaviorSplit: | |
| 425 | + | // Split pane behavior (default) | |
| 426 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 427 | + | if direction == "" { | |
| 428 | + | direction = "right" | |
| 429 | + | } | |
| 430 | + | if err := wm.insertPopupPane(info.parentNode, popupPane, direction); err != nil { | |
| 431 | + | log.Printf("[workspace] Failed to insert split popup pane: %v", err) | |
| 432 | + | return | |
| 433 | + | } | |
| 434 | + | ||
| 435 | + | case config.PopupBehaviorStacked: | |
| 436 | + | // Stacked pane behavior | |
| 437 | + | _, err := wm.stackedPaneManager.StackPane(info.parentNode) | |
| 438 | + | if err != nil { | |
| 439 | + | log.Printf("[workspace] Failed to stack existing pane: %v", err) | |
| 440 | + | return | |
| 441 | + | } | |
| 442 | + | // Insert the popup pane in the same container | |
| 443 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 444 | + | if direction == "" { | |
| 445 | + | direction = "right" | |
| 446 | + | } | |
| 447 | + | if err := wm.insertPopupPane(info.parentNode, popupPane, direction); err != nil { | |
| 448 | + | log.Printf("[workspace] Failed to insert stacked popup pane: %v", err) | |
| 449 | + | return | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | case config.PopupBehaviorTabbed: | |
| 453 | + | // Tabbed behavior - TODO: implement proper tabbed interface | |
| 454 | + | log.Printf("[workspace] WARNING: Tabbed popup behavior not yet implemented, falling back to split") | |
| 455 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 456 | + | if direction == "" { | |
| 457 | + | direction = "right" | |
| 458 | + | } | |
| 459 | + | if err := wm.insertPopupPane(info.parentNode, popupPane, direction); err != nil { | |
| 460 | + | log.Printf("[workspace] Failed to insert tabbed popup pane: %v", err) | |
| 461 | + | return | |
| 462 | + | } | |
| 463 | + | ||
| 464 | + | case config.PopupBehaviorWindowed: | |
| 465 | + | // Windowed behavior - opens in new workspace/window | |
| 466 | + | // TODO: implement new window creation with GTK | |
| 467 | + | log.Printf("[workspace] WARNING: Windowed popup behavior not yet implemented, falling back to split") | |
| 468 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 469 | + | if direction == "" { | |
| 470 | + | direction = "right" | |
| 471 | + | } | |
| 472 | + | if err := wm.insertPopupPane(info.parentNode, popupPane, direction); err != nil { | |
| 473 | + | log.Printf("[workspace] Failed to insert windowed popup pane: %v", err) | |
| 474 | + | return | |
| 475 | + | } | |
| 476 | + | ||
| 477 | + | default: | |
| 478 | + | // Fallback to split if unknown behavior | |
| 479 | + | log.Printf("[workspace] Unknown popup behavior '%s', falling back to split", behavior) | |
| 480 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |
| 481 | + | if direction == "" { | |
| 482 | + | direction = "right" | |
| 483 | + | } | |
| 484 | + | if err := wm.insertPopupPane(info.parentNode, popupPane, direction); err != nil { | |
| 485 | + | log.Printf("[workspace] Failed to insert popup pane: %v", err) | |
| 486 | + | return | |
| 487 | + | } | |
| 488 | + | } | |
| 489 | + | ||
| 490 | + | // Configure the popup node | |
| 491 | + | node := wm.viewToNode[wrappedView] | |
| 492 | + | if node != nil { | |
| 493 | + | node.windowType = webkit.WindowTypePopup | |
| 494 | + | node.isRelated = true | |
| 495 | + | node.parentPane = info.parentNode | |
| 496 | + | node.isPopup = true | |
| 497 | + | node.autoClose = wm.shouldAutoClose(info.url) | |
| 498 | + | node.popupID = popupID // Store popupID for close signal lookup | |
| 499 | + | ||
| 500 | + | // Track this popup in the parent's activePopupChildren | |
| 501 | + | if info.parentNode != nil { | |
| 502 | + | popupWebViewID := wrappedView.IDString() | |
| 503 | + | if info.parentNode.activePopupChildren == nil { | |
| 504 | + | info.parentNode.activePopupChildren = make([]string, 0) | |
| 505 | + | } | |
| 506 | + | info.parentNode.activePopupChildren = append(info.parentNode.activePopupChildren, popupWebViewID) | |
| 507 | + | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", | |
| 508 | + | popupWebViewID, len(info.parentNode.activePopupChildren)) | |
| 509 | + | ||
| 510 | + | // Register navigation policy handler for parent | |
| 511 | + | if info.parentNode.pane != nil && info.parentNode.pane.WebView() != nil { | |
| 512 | + | info.parentNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |
| 513 | + | if isUserGesture { | |
| 514 | + | return true | |
| 515 | + | } | |
| 516 | + | if len(info.parentNode.activePopupChildren) > 0 { | |
| 517 | + | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", | |
| 518 | + | len(info.parentNode.activePopupChildren), url) | |
| 519 | + | return false | |
| 520 | + | } | |
| 521 | + | return true | |
| 522 | + | }) | |
| 523 | + | } | |
| 524 | + | } | |
| 525 | + | ||
| 526 | + | // URL-based auto-close for OAuth popups | |
| 527 | + | if node.autoClose { | |
| 528 | + | wm.registerOAuthAutoClose(wrappedView, info.url) | |
| 529 | + | } | |
| 530 | + | } | |
| 531 | + | ||
| 532 | + | wm.ensureGUIInPane(popupPane) | |
| 533 | + | ||
| 534 | + | // WebKit handles loading the URL automatically, just ensure visibility | |
| 535 | + | if err := wrappedView.Show(); err != nil { | |
| 536 | + | log.Printf("[workspace] failed to show popup WebView: %v", err) | |
| 537 | + | } | |
| 538 | + | ||
| 539 | + | log.Printf("[workspace] Popup pane inserted and visible for URL: %s", info.url) | |
| 540 | + | } | |
| 541 | + | ||
| 542 | + | // handlePopupClose responds to WebKit's close signal | |
| 543 | + | func (wm *WorkspaceManager) handlePopupClose(popupID uint64) { | |
| 544 | + | log.Printf("[workspace] close signal received for popup native=%d", popupID) | |
| 545 | + | ||
| 546 | + | // Check if it's in pending (not yet shown) | |
| 547 | + | if info, ok := wm.pendingPopups[popupID]; ok { | |
| 548 | + | delete(wm.pendingPopups, popupID) | |
| 549 | + | log.Printf("[workspace] Popup closed before ready-to-show, cleaning up") | |
| 550 | + | // Just remove from pending, WebKit will handle cleanup | |
| 551 | + | _ = info // avoid unused variable | |
| 552 | + | return | |
| 553 | + | } | |
| 554 | + | ||
| 555 | + | // Find pane by popupID (stored in node during ready-to-show) | |
| 556 | + | var targetNode *paneNode | |
| 557 | + | for _, node := range wm.viewToNode { | |
| 558 | + | if node != nil && node.popupID == popupID { | |
| 559 | + | targetNode = node | |
| 560 | + | break | |
| 561 | + | } | |
| 562 | + | } | |
| 563 | + | ||
| 564 | + | if targetNode == nil { | |
| 565 | + | log.Printf("[workspace] close signal for unknown popup ID=%d, already closed?", popupID) | |
| 566 | + | return | |
| 567 | + | } | |
| 568 | + | ||
| 569 | + | // Remove from parent's activePopupChildren | |
| 570 | + | // Find the wrapped WebView for this node to get its IDString | |
| 571 | + | var wrappedViewID string | |
| 572 | + | for webView, node := range wm.viewToNode { | |
| 573 | + | if node == targetNode { | |
| 574 | + | wrappedViewID = webView.IDString() | |
| 575 | + | break | |
| 576 | + | } | |
| 577 | + | } | |
| 578 | + | ||
| 579 | + | if targetNode.parentPane != nil && wrappedViewID != "" { | |
| 580 | + | for i, childID := range targetNode.parentPane.activePopupChildren { | |
| 581 | + | if childID == wrappedViewID { | |
| 582 | + | targetNode.parentPane.activePopupChildren = append( | |
| 583 | + | targetNode.parentPane.activePopupChildren[:i], | |
| 584 | + | targetNode.parentPane.activePopupChildren[i+1:]...) | |
| 585 | + | log.Printf("[workspace] Removed popup from parent's activePopupChildren (remaining: %d)", | |
| 586 | + | len(targetNode.parentPane.activePopupChildren)) | |
| 587 | + | break | |
| 588 | + | } | |
| 589 | + | } | |
| 590 | + | } | |
| 591 | + | ||
| 592 | + | // Clean close via existing ClosePane (WebKit already stopped) | |
| 593 | + | log.Printf("[workspace] Closing popup pane via ClosePane") | |
| 594 | + | if err := wm.ClosePane(targetNode); err != nil { | |
| 595 | + | log.Printf("[workspace] Error closing popup pane: %v", err) | |
| 596 | + | } | |
| 597 | + | } | |
| @@ -2,13 +2,14 @@ | |||||
| 2 | package browser | 2 | package browser | ||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | - | "fmt" | |||
| 6 | "log" | 5 | "log" | ||
| 7 | "strings" | 6 | "strings" | ||
| 8 | "time" | 7 | "time" | ||
| 9 | 8 | ||||
| 10 | - | "github.com/bnema/dumber/internal/app/messaging" | 9 | + | "github.com/bnema/dumber/internal/config" |
| 11 | "github.com/bnema/dumber/pkg/webkit" | 10 | "github.com/bnema/dumber/pkg/webkit" | ||
| 11 | + | webkitgtk "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | |||
| 12 | + | "github.com/diamondburned/gotk4/pkg/glib/v2" | |||
| 12 | ) | 13 | ) | ||
| 13 | 14 | ||||
| 14 | const ( | 15 | const ( | ||
| @@ -17,264 +18,9 @@ const ( | |||||
| 17 | defaultPopupMinHeight = 600 // Minimum height for OAuth popups | 18 | defaultPopupMinHeight = 600 // Minimum height for OAuth popups | ||
| 18 | ) | 19 | ) | ||
| 19 | 20 | ||||
| 20 | - | // HandlePopup handles popup window creation requests from WebViews | 21 | + | // HandlePopup is DEPRECATED and removed. |
| 21 | - | func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *webkit.WebView { | 22 | + | // Popup lifecycle now uses WebKit's native create/ready-to-show/close signals. |
| 22 | - | log.Printf("[workspace] HandlePopup called for URL: %s", url) | 23 | + | // See setupPopupHandling(), handlePopupReadyToShow(), and handlePopupClose() instead. |
| 23 | - | ||||
| 24 | - | // Check for frame type markers added by WebKit layer | |||
| 25 | - | isBlankTarget := strings.HasSuffix(url, "#__dumber_frame_blank") | |||
| 26 | - | isPopupTarget := strings.HasSuffix(url, "#__dumber_frame_popup") | |||
| 27 | - | ||||
| 28 | - | // Clean the URL by removing our markers | |||
| 29 | - | if isBlankTarget || isPopupTarget { | |||
| 30 | - | if isBlankTarget { | |||
| 31 | - | url = strings.TrimSuffix(url, "#__dumber_frame_blank") | |||
| 32 | - | log.Printf("[workspace] Detected _blank target - will create regular pane for: %s", url) | |||
| 33 | - | } else { | |||
| 34 | - | url = strings.TrimSuffix(url, "#__dumber_frame_popup") | |||
| 35 | - | log.Printf("[workspace] Detected popup target - will create popup pane for: %s", url) | |||
| 36 | - | } | |||
| 37 | - | } | |||
| 38 | - | ||||
| 39 | - | if wm == nil || source == nil { | |||
| 40 | - | log.Printf("[workspace] HandlePopup: nil workspace manager or source - allowing native popup") | |||
| 41 | - | return nil | |||
| 42 | - | } | |||
| 43 | - | ||||
| 44 | - | node := wm.viewToNode[source] | |||
| 45 | - | if node == nil { | |||
| 46 | - | log.Printf("[workspace] popup from unknown webview - allowing native popup") | |||
| 47 | - | return nil | |||
| 48 | - | } | |||
| 49 | - | ||||
| 50 | - | // Note: HandlePopup is now obsolete - window.open is handled directly via JavaScript bypass | |||
| 51 | - | ||||
| 52 | - | cfg := wm.app.config | |||
| 53 | - | if cfg == nil { | |||
| 54 | - | log.Printf("[workspace] HandlePopup: nil config - allowing native popup") | |||
| 55 | - | return nil | |||
| 56 | - | } | |||
| 57 | - | ||||
| 58 | - | popCfg := cfg.Workspace.Popups | |||
| 59 | - | log.Printf("[workspace] Popup config - OpenInNewPane: %v, Placement: %s", popCfg.OpenInNewPane, popCfg.Placement) | |||
| 60 | - | ||||
| 61 | - | if !popCfg.OpenInNewPane { | |||
| 62 | - | log.Printf("[workspace] Popup creation disabled in config - allowing native popup") | |||
| 63 | - | return nil | |||
| 64 | - | } | |||
| 65 | - | ||||
| 66 | - | // Smart detection path: create temporary view and decide placement once type is known | |||
| 67 | - | if popCfg.EnableSmartDetection { | |||
| 68 | - | webkitCfg, err := wm.app.buildWebkitConfig() | |||
| 69 | - | if err != nil { | |||
| 70 | - | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |||
| 71 | - | return nil | |||
| 72 | - | } | |||
| 73 | - | // Popups are embedded in workspace paned containers - no separate window needed | |||
| 74 | - | webkitCfg.CreateWindow = false | |||
| 75 | - | // Create as related to avoid WindowFeatures crash; we'll decide final placement later | |||
| 76 | - | newView, err := webkit.NewWebViewWithRelated(webkitCfg, source) | |||
| 77 | - | if err != nil { | |||
| 78 | - | log.Printf("[workspace] failed to create temp WebView: %v - allowing native popup", err) | |||
| 79 | - | return nil | |||
| 80 | - | } | |||
| 81 | - | ||||
| 82 | - | // Register detection callback | |||
| 83 | - | newView.OnWindowTypeDetected(func(t webkit.WindowType, feat *webkit.WindowFeatures) { | |||
| 84 | - | wm.RunOnUI(func() { | |||
| 85 | - | wm.handleDetectedWindowType(node, newView, url, t, feat) | |||
| 86 | - | }) | |||
| 87 | - | }) | |||
| 88 | - | ||||
| 89 | - | // Fallback: if detection never fires, treat as popup as before | |||
| 90 | - | go func() { | |||
| 91 | - | time.Sleep(1500 * time.Millisecond) | |||
| 92 | - | if newView != nil { | |||
| 93 | - | wm.RunOnUI(func() { | |||
| 94 | - | if wm.viewToNode[newView] == nil { // not yet placed | |||
| 95 | - | wm.handleDetectedWindowType(node, newView, url, webkit.WindowTypePopup, nil) | |||
| 96 | - | } | |||
| 97 | - | }) | |||
| 98 | - | } | |||
| 99 | - | }() | |||
| 100 | - | ||||
| 101 | - | return newView | |||
| 102 | - | } | |||
| 103 | - | ||||
| 104 | - | // Legacy path preserved | |||
| 105 | - | webkitCfg, err := wm.app.buildWebkitConfig() | |||
| 106 | - | if err != nil { | |||
| 107 | - | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |||
| 108 | - | return nil | |||
| 109 | - | } | |||
| 110 | - | // Popups are embedded in workspace paned containers - no separate window needed | |||
| 111 | - | webkitCfg.CreateWindow = false | |||
| 112 | - | newView, err := webkit.NewWebViewWithRelated(webkitCfg, source) | |||
| 113 | - | if err != nil { | |||
| 114 | - | log.Printf("[workspace] failed to create placeholder WebView: %v - allowing native popup", err) | |||
| 115 | - | return nil | |||
| 116 | - | } | |||
| 117 | - | ||||
| 118 | - | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler | |||
| 119 | - | ||||
| 120 | - | // Create a pane for the new WebView | |||
| 121 | - | newPane, err := wm.createPane(newView) | |||
| 122 | - | if err != nil { | |||
| 123 | - | log.Printf("[workspace] failed to create popup pane: %v - allowing native popup", err) | |||
| 124 | - | return nil | |||
| 125 | - | } | |||
| 126 | - | ||||
| 127 | - | // Determine placement direction | |||
| 128 | - | direction := strings.ToLower(popCfg.Placement) | |||
| 129 | - | if direction == "" { | |||
| 130 | - | direction = "right" | |||
| 131 | - | } | |||
| 132 | - | ||||
| 133 | - | // Determine target node for splitting | |||
| 134 | - | target := node | |||
| 135 | - | if !popCfg.FollowPaneContext && wm.GetActiveNode() != nil { | |||
| 136 | - | target = wm.GetActiveNode() | |||
| 137 | - | } | |||
| 138 | - | ||||
| 139 | - | // Add the popup pane to the workspace using manual pane insertion | |||
| 140 | - | if err := wm.insertPopupPane(target, newPane, direction); err != nil { | |||
| 141 | - | log.Printf("[workspace] popup pane insertion failed: %v - allowing native popup", err) | |||
| 142 | - | return nil | |||
| 143 | - | } | |||
| 144 | - | ||||
| 145 | - | // Get the new node and popup WebView ID for tracking | |||
| 146 | - | newNode := wm.viewToNode[newView] | |||
| 147 | - | popupWebViewID := newView.IDString() | |||
| 148 | - | ||||
| 149 | - | // Apply different behavior based on target type | |||
| 150 | - | if isBlankTarget { | |||
| 151 | - | log.Printf("[workspace] Treating _blank target as regular pane - no auto-close behavior") | |||
| 152 | - | // For _blank targets, set up parent relationship but no auto-close | |||
| 153 | - | if newNode != nil { | |||
| 154 | - | newNode.parentPane = node | |||
| 155 | - | } | |||
| 156 | - | ||||
| 157 | - | // Register close handler to clean up activePopupChildren tracking | |||
| 158 | - | newView.RegisterCloseHandler(func() { | |||
| 159 | - | log.Printf("[workspace] _blank popup requesting close via window.close()") | |||
| 160 | - | // Remove this popup from parent's activePopupChildren | |||
| 161 | - | if node != nil && popupWebViewID != "" { | |||
| 162 | - | for i, childID := range node.activePopupChildren { | |||
| 163 | - | if childID == popupWebViewID { | |||
| 164 | - | node.activePopupChildren = append(node.activePopupChildren[:i], node.activePopupChildren[i+1:]...) | |||
| 165 | - | log.Printf("[workspace] Removed _blank popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(node.activePopupChildren)) | |||
| 166 | - | break | |||
| 167 | - | } | |||
| 168 | - | } | |||
| 169 | - | } | |||
| 170 | - | }) | |||
| 171 | - | } else { | |||
| 172 | - | log.Printf("[workspace] Treating as popup pane - applying popup-specific behavior") | |||
| 173 | - | // Mark as popup for auto-close handling (OAuth flows, etc.) | |||
| 174 | - | if newNode != nil { | |||
| 175 | - | newNode.isPopup = true | |||
| 176 | - | newNode.parentPane = node | |||
| 177 | - | log.Printf("[workspace] Marked pane as popup for auto-close handling") | |||
| 178 | - | ||||
| 179 | - | // Register close handler for popup auto-close on window.close() | |||
| 180 | - | newView.RegisterCloseHandler(func() { | |||
| 181 | - | log.Printf("[workspace] Popup requesting close via window.close()") | |||
| 182 | - | ||||
| 183 | - | // Remove this popup from parent's activePopupChildren | |||
| 184 | - | if node != nil && popupWebViewID != "" { | |||
| 185 | - | for i, childID := range node.activePopupChildren { | |||
| 186 | - | if childID == popupWebViewID { | |||
| 187 | - | node.activePopupChildren = append(node.activePopupChildren[:i], node.activePopupChildren[i+1:]...) | |||
| 188 | - | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(node.activePopupChildren)) | |||
| 189 | - | break | |||
| 190 | - | } | |||
| 191 | - | } | |||
| 192 | - | } | |||
| 193 | - | ||||
| 194 | - | // Look up the node at close time | |||
| 195 | - | if node := wm.viewToNode[newView]; node != nil && node.isPopup { | |||
| 196 | - | log.Printf("[workspace] Closing popup pane") | |||
| 197 | - | // Brief delay to allow any final redirects to complete | |||
| 198 | - | time.AfterFunc(200*time.Millisecond, func() { | |||
| 199 | - | wm.scheduleIdleGuarded(func() bool { | |||
| 200 | - | if node == nil || !node.widgetValid { | |||
| 201 | - | return false | |||
| 202 | - | } | |||
| 203 | - | if err := wm.ClosePane(node); err != nil { | |||
| 204 | - | log.Printf("[workspace] Failed to close popup pane: %v", err) | |||
| 205 | - | } | |||
| 206 | - | return false | |||
| 207 | - | }, node) | |||
| 208 | - | }) | |||
| 209 | - | } else { | |||
| 210 | - | log.Printf("[workspace] Could not find popup node for close handler") | |||
| 211 | - | } | |||
| 212 | - | }) | |||
| 213 | - | } else { | |||
| 214 | - | log.Printf("[workspace] Warning: could not find node for popup WebView in viewToNode map") | |||
| 215 | - | } | |||
| 216 | - | } | |||
| 217 | - | ||||
| 218 | - | // Track this popup in the parent's activePopupChildren (for both _blank and popup types) | |||
| 219 | - | // This is critical to prevent OAuth flows from hijacking the parent pane via window.opener | |||
| 220 | - | if node != nil && popupWebViewID != "" { | |||
| 221 | - | if node.activePopupChildren == nil { | |||
| 222 | - | node.activePopupChildren = make([]string, 0) | |||
| 223 | - | } | |||
| 224 | - | node.activePopupChildren = append(node.activePopupChildren, popupWebViewID) | |||
| 225 | - | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(node.activePopupChildren)) | |||
| 226 | - | ||||
| 227 | - | // Register navigation policy handler for parent to block script-initiated navigations | |||
| 228 | - | // when it has active popup children (prevents OAuth from hijacking parent pane) | |||
| 229 | - | if node.pane != nil && node.pane.WebView() != nil { | |||
| 230 | - | node.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |||
| 231 | - | // Always allow user-initiated navigations | |||
| 232 | - | if isUserGesture { | |||
| 233 | - | return true | |||
| 234 | - | } | |||
| 235 | - | ||||
| 236 | - | // Block script-initiated navigations if this pane has active popup children | |||
| 237 | - | if len(node.activePopupChildren) > 0 { | |||
| 238 | - | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(node.activePopupChildren), url) | |||
| 239 | - | return false | |||
| 240 | - | } | |||
| 241 | - | ||||
| 242 | - | // Allow if no active popups | |||
| 243 | - | return true | |||
| 244 | - | }) | |||
| 245 | - | log.Printf("[workspace] Registered navigation policy handler for parent pane") | |||
| 246 | - | } | |||
| 247 | - | } | |||
| 248 | - | ||||
| 249 | - | // Ensure GUI components are available in the new pane | |||
| 250 | - | wm.ensureGUIInPane(newPane) | |||
| 251 | - | ||||
| 252 | - | // Inject GUI components into the popup pane | |||
| 253 | - | wm.ensureGUIInPane(newPane) | |||
| 254 | - | ||||
| 255 | - | // Load the URL if provided | |||
| 256 | - | if url != "" { | |||
| 257 | - | paneType := "popup" | |||
| 258 | - | if isBlankTarget { | |||
| 259 | - | paneType = "_blank target" | |||
| 260 | - | } | |||
| 261 | - | log.Printf("[webkit] LoadURL (%s): %s", paneType, url) | |||
| 262 | - | if err := newView.LoadURL(url); err != nil { | |||
| 263 | - | log.Printf("[workspace] failed to load %s URL: %v", paneType, err) | |||
| 264 | - | } | |||
| 265 | - | // Ensure the WebView is visible after loading URL | |||
| 266 | - | if err := newView.Show(); err != nil { | |||
| 267 | - | log.Printf("[workspace] failed to show popup WebView: %v", err) | |||
| 268 | - | } | |||
| 269 | - | } | |||
| 270 | - | ||||
| 271 | - | if isBlankTarget { | |||
| 272 | - | log.Printf("[workspace] Created regular pane for _blank target URL: %s", url) | |||
| 273 | - | } else { | |||
| 274 | - | log.Printf("[workspace] Created popup pane for URL: %s", url) | |||
| 275 | - | } | |||
| 276 | - | return newView | |||
| 277 | - | } | |||
| 278 | 24 | ||||
| 279 | // registerOAuthAutoClose sets up OAuth auto-close functionality for popups | 25 | // registerOAuthAutoClose sets up OAuth auto-close functionality for popups | ||
| 280 | // Note: OAuth detection is now handled by the main-world.js injection script | 26 | // Note: OAuth detection is now handled by the main-world.js injection script | ||
| @@ -283,296 +29,6 @@ func (wm *WorkspaceManager) registerOAuthAutoClose(view *webkit.WebView, url str | |||||
| 283 | log.Printf("[workspace] OAuth detection will be handled by main-world.js injection script") | 29 | log.Printf("[workspace] OAuth detection will be handled by main-world.js injection script") | ||
| 284 | } | 30 | } | ||
| 285 | 31 | ||||
| 286 | - | // applyWindowFeatures applies window features to a WebView based on intent | |||
| 287 | - | func (wm *WorkspaceManager) applyWindowFeatures(view *webkit.WebView, intent *messaging.WindowIntent, isPopup bool) { | |||
| 288 | - | if intent == nil { | |||
| 289 | - | return | |||
| 290 | - | } | |||
| 291 | - | ||||
| 292 | - | features := &webkit.WindowFeatures{} | |||
| 293 | - | ||||
| 294 | - | // Apply dimensions if specified | |||
| 295 | - | if intent.Width != nil { | |||
| 296 | - | features.Width = *intent.Width | |||
| 297 | - | } | |||
| 298 | - | if intent.Height != nil { | |||
| 299 | - | features.Height = *intent.Height | |||
| 300 | - | } | |||
| 301 | - | ||||
| 302 | - | // Apply visibility features based on window type | |||
| 303 | - | defaultToolbar := !isPopup | |||
| 304 | - | defaultLocation := !isPopup | |||
| 305 | - | defaultMenubar := !isPopup | |||
| 306 | - | ||||
| 307 | - | if intent.Toolbar != nil { | |||
| 308 | - | features.ToolbarVisible = *intent.Toolbar | |||
| 309 | - | } else { | |||
| 310 | - | features.ToolbarVisible = defaultToolbar | |||
| 311 | - | } | |||
| 312 | - | ||||
| 313 | - | if intent.Location != nil { | |||
| 314 | - | features.LocationbarVisible = *intent.Location | |||
| 315 | - | } else { | |||
| 316 | - | features.LocationbarVisible = defaultLocation | |||
| 317 | - | } | |||
| 318 | - | ||||
| 319 | - | if intent.Menubar != nil { | |||
| 320 | - | features.MenubarVisible = *intent.Menubar | |||
| 321 | - | } else { | |||
| 322 | - | features.MenubarVisible = defaultMenubar | |||
| 323 | - | } | |||
| 324 | - | ||||
| 325 | - | if intent.Resizable != nil { | |||
| 326 | - | features.Resizable = *intent.Resizable | |||
| 327 | - | } else { | |||
| 328 | - | features.Resizable = true // Usually resizable unless explicitly disabled | |||
| 329 | - | } | |||
| 330 | - | ||||
| 331 | - | view.SetWindowFeatures(features) | |||
| 332 | - | windowTypeStr := "tab" | |||
| 333 | - | if isPopup { | |||
| 334 | - | windowTypeStr = "popup" | |||
| 335 | - | } | |||
| 336 | - | log.Printf("[workspace] Applied %s window features from intent: size=%dx%d, toolbar=%t, location=%t, menubar=%t, resizable=%t", | |||
| 337 | - | windowTypeStr, features.Width, features.Height, features.ToolbarVisible, features.LocationbarVisible, features.MenubarVisible, features.Resizable) | |||
| 338 | - | } | |||
| 339 | - | ||||
| 340 | - | // handleIntentAsTab creates a tab pane based on window.open intent | |||
| 341 | - | func (wm *WorkspaceManager) handleIntentAsTab(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView { | |||
| 342 | - | log.Printf("[workspace] Handling intent as tab: %s", url) | |||
| 343 | - | ||||
| 344 | - | webkitCfg, err := wm.app.buildWebkitConfig() | |||
| 345 | - | if err != nil { | |||
| 346 | - | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |||
| 347 | - | return nil | |||
| 348 | - | } | |||
| 349 | - | // Tabs are embedded in workspace paned containers - no separate window needed | |||
| 350 | - | webkitCfg.CreateWindow = false | |||
| 351 | - | ||||
| 352 | - | newView, err := webkit.NewWebView(webkitCfg) | |||
| 353 | - | if err != nil { | |||
| 354 | - | log.Printf("[workspace] failed to create tab WebView: %v - allowing native popup", err) | |||
| 355 | - | return nil | |||
| 356 | - | } | |||
| 357 | - | ||||
| 358 | - | newPane, err := wm.createPane(newView) | |||
| 359 | - | if err != nil { | |||
| 360 | - | log.Printf("[workspace] failed to create tab pane: %v - allowing native popup", err) | |||
| 361 | - | return nil | |||
| 362 | - | } | |||
| 363 | - | ||||
| 364 | - | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 365 | - | if direction == "" { | |||
| 366 | - | direction = "right" | |||
| 367 | - | } | |||
| 368 | - | ||||
| 369 | - | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { | |||
| 370 | - | log.Printf("[workspace] tab pane insertion failed: %v - allowing native popup", err) | |||
| 371 | - | return nil | |||
| 372 | - | } | |||
| 373 | - | ||||
| 374 | - | node := wm.viewToNode[newView] | |||
| 375 | - | if node != nil { | |||
| 376 | - | node.windowType = webkit.WindowTypeTab | |||
| 377 | - | node.isRelated = false | |||
| 378 | - | ||||
| 379 | - | // Apply window features from JavaScript intent | |||
| 380 | - | wm.applyWindowFeatures(newView, intent, false) | |||
| 381 | - | } | |||
| 382 | - | ||||
| 383 | - | wm.ensureGUIInPane(newPane) | |||
| 384 | - | ||||
| 385 | - | if url != "" { | |||
| 386 | - | if err := newView.LoadURL(url); err != nil { | |||
| 387 | - | log.Printf("[workspace] failed to load tab URL: %v", err) | |||
| 388 | - | } | |||
| 389 | - | if err := newView.Show(); err != nil { | |||
| 390 | - | log.Printf("[workspace] failed to show tab WebView: %v", err) | |||
| 391 | - | } | |||
| 392 | - | } | |||
| 393 | - | ||||
| 394 | - | log.Printf("[workspace] Created tab pane for URL: %s", url) | |||
| 395 | - | return newView | |||
| 396 | - | } | |||
| 397 | - | ||||
| 398 | - | // handleIntentAsPopup creates a related popup pane based on window.open intent | |||
| 399 | - | func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string, intent *messaging.WindowIntent) *webkit.WebView { | |||
| 400 | - | log.Printf("[workspace] Handling intent as popup: %s", url) | |||
| 401 | - | ||||
| 402 | - | webkitCfg, err := wm.app.buildWebkitConfig() | |||
| 403 | - | if err != nil { | |||
| 404 | - | log.Printf("[workspace] failed to build webkit config: %v - allowing native popup", err) | |||
| 405 | - | return nil | |||
| 406 | - | } | |||
| 407 | - | // Popups are embedded in workspace paned containers - no separate window needed | |||
| 408 | - | webkitCfg.CreateWindow = false | |||
| 409 | - | ||||
| 410 | - | newView, err := webkit.NewWebViewWithRelated(webkitCfg, sourceNode.pane.webView) | |||
| 411 | - | if err != nil { | |||
| 412 | - | log.Printf("[workspace] failed to create popup WebView: %v - allowing native popup", err) | |||
| 413 | - | return nil | |||
| 414 | - | } | |||
| 415 | - | ||||
| 416 | - | // Log the parent-popup WebView ID relationship for OAuth auto-close | |||
| 417 | - | parentWebViewID := sourceNode.pane.webView.IDString() | |||
| 418 | - | popupWebViewID := newView.IDString() | |||
| 419 | - | log.Printf("[workspace] Created popup WebView: parentID=%s popupID=%s url=%s", parentWebViewID, popupWebViewID, url) | |||
| 420 | - | ||||
| 421 | - | // Store popup WebView ID in parent's localStorage for OAuth callback lookup | |||
| 422 | - | storeScript := fmt.Sprintf(` | |||
| 423 | - | try { | |||
| 424 | - | const parentWebViewId = '%s'; | |||
| 425 | - | const popupWebViewId = '%s'; | |||
| 426 | - | const popupMapping = { | |||
| 427 | - | parentId: parentWebViewId, | |||
| 428 | - | popupId: popupWebViewId, | |||
| 429 | - | timestamp: Date.now(), | |||
| 430 | - | url: '%s' | |||
| 431 | - | }; | |||
| 432 | - | localStorage.setItem('popup_mapping_' + parentWebViewId, JSON.stringify(popupMapping)); | |||
| 433 | - | console.log('[workspace] Stored popup mapping:', popupMapping); | |||
| 434 | - | } catch(e) { | |||
| 435 | - | console.warn('[workspace] Failed to store popup mapping:', e); | |||
| 436 | - | } | |||
| 437 | - | `, parentWebViewID, popupWebViewID, url) | |||
| 438 | - | ||||
| 439 | - | // Inject into parent WebView so it can find its popup later | |||
| 440 | - | if err := sourceNode.pane.webView.InjectScript(storeScript); err != nil { | |||
| 441 | - | log.Printf("[workspace] Failed to inject popup mapping script into parent: %v", err) | |||
| 442 | - | } | |||
| 443 | - | ||||
| 444 | - | newPane, err := wm.createPane(newView) | |||
| 445 | - | if err != nil { | |||
| 446 | - | log.Printf("[workspace] failed to create popup pane: %v - allowing native popup", err) | |||
| 447 | - | return nil | |||
| 448 | - | } | |||
| 449 | - | ||||
| 450 | - | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 451 | - | if direction == "" { | |||
| 452 | - | direction = "right" | |||
| 453 | - | } | |||
| 454 | - | ||||
| 455 | - | if err := wm.insertPopupPane(sourceNode, newPane, direction); err != nil { | |||
| 456 | - | log.Printf("[workspace] popup pane insertion failed: %v - allowing native popup", err) | |||
| 457 | - | return nil | |||
| 458 | - | } | |||
| 459 | - | ||||
| 460 | - | node := wm.viewToNode[newView] | |||
| 461 | - | var requestID string | |||
| 462 | - | if node != nil { | |||
| 463 | - | node.windowType = webkit.WindowTypePopup | |||
| 464 | - | node.isRelated = true | |||
| 465 | - | node.parentPane = sourceNode | |||
| 466 | - | node.isPopup = true | |||
| 467 | - | node.autoClose = wm.shouldAutoClose(url) | |||
| 468 | - | ||||
| 469 | - | // Track this popup in the parent's activePopupChildren | |||
| 470 | - | if sourceNode != nil { | |||
| 471 | - | if sourceNode.activePopupChildren == nil { | |||
| 472 | - | sourceNode.activePopupChildren = make([]string, 0) | |||
| 473 | - | } | |||
| 474 | - | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren, popupWebViewID) | |||
| 475 | - | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |||
| 476 | - | ||||
| 477 | - | // Register navigation policy handler for parent to block script-initiated navigations | |||
| 478 | - | // when it has active popup children (prevents OAuth from hijacking parent pane) | |||
| 479 | - | if sourceNode.pane != nil && sourceNode.pane.WebView() != nil { | |||
| 480 | - | sourceNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |||
| 481 | - | // Always allow user-initiated navigations | |||
| 482 | - | if isUserGesture { | |||
| 483 | - | return true | |||
| 484 | - | } | |||
| 485 | - | ||||
| 486 | - | // Block script-initiated navigations if this pane has active popup children | |||
| 487 | - | if len(sourceNode.activePopupChildren) > 0 { | |||
| 488 | - | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url) | |||
| 489 | - | return false | |||
| 490 | - | } | |||
| 491 | - | ||||
| 492 | - | // Allow if no active popups | |||
| 493 | - | return true | |||
| 494 | - | }) | |||
| 495 | - | log.Printf("[workspace] Registered navigation policy handler for parent pane") | |||
| 496 | - | } | |||
| 497 | - | } | |||
| 498 | - | ||||
| 499 | - | // Store requestID for deduplication cleanup | |||
| 500 | - | if intent != nil { | |||
| 501 | - | requestID = intent.RequestID | |||
| 502 | - | node.requestID = requestID | |||
| 503 | - | } | |||
| 504 | - | ||||
| 505 | - | // Apply window features from JavaScript intent | |||
| 506 | - | wm.applyWindowFeatures(newView, intent, true) | |||
| 507 | - | ||||
| 508 | - | // Apply minimum size constraints to prevent compression | |||
| 509 | - | var width, height int | |||
| 510 | - | if intent != nil { | |||
| 511 | - | if intent.Width != nil { | |||
| 512 | - | width = *intent.Width | |||
| 513 | - | } | |||
| 514 | - | if intent.Height != nil { | |||
| 515 | - | height = *intent.Height | |||
| 516 | - | } | |||
| 517 | - | } | |||
| 518 | - | wm.applyPopupSizeConstraints(newView, width, height) | |||
| 519 | - | } | |||
| 520 | - | ||||
| 521 | - | // Register close handler for popup auto-close | |||
| 522 | - | newView.RegisterCloseHandler(func() { | |||
| 523 | - | log.Printf("[workspace] Popup requesting close via window.close()") | |||
| 524 | - | ||||
| 525 | - | // Clear the RequestID from deduplicator to allow new popups with same ID | |||
| 526 | - | if requestID != "" && wm.paneDeduplicator != nil { | |||
| 527 | - | wm.paneDeduplicator.ClearRequestID(requestID) | |||
| 528 | - | } | |||
| 529 | - | ||||
| 530 | - | // Remove this popup from parent's activePopupChildren | |||
| 531 | - | if sourceNode != nil && popupWebViewID != "" { | |||
| 532 | - | for i, childID := range sourceNode.activePopupChildren { | |||
| 533 | - | if childID == popupWebViewID { | |||
| 534 | - | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren[:i], sourceNode.activePopupChildren[i+1:]...) | |||
| 535 | - | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |||
| 536 | - | break | |||
| 537 | - | } | |||
| 538 | - | } | |||
| 539 | - | } | |||
| 540 | - | ||||
| 541 | - | if n := wm.viewToNode[newView]; n != nil && n.isPopup { | |||
| 542 | - | time.AfterFunc(200*time.Millisecond, func() { | |||
| 543 | - | wm.scheduleIdleGuarded(func() bool { | |||
| 544 | - | if n == nil || !n.widgetValid { | |||
| 545 | - | return false | |||
| 546 | - | } | |||
| 547 | - | if err := wm.ClosePane(n); err != nil { | |||
| 548 | - | log.Printf("[workspace] Failed to close popup pane: %v", err) | |||
| 549 | - | } | |||
| 550 | - | return false | |||
| 551 | - | }, n) | |||
| 552 | - | }) | |||
| 553 | - | } | |||
| 554 | - | }) | |||
| 555 | - | ||||
| 556 | - | // URL-based auto-close for OAuth popups | |||
| 557 | - | if node != nil && node.isPopup && node.autoClose { | |||
| 558 | - | wm.registerOAuthAutoClose(newView, url) | |||
| 559 | - | } | |||
| 560 | - | ||||
| 561 | - | wm.ensureGUIInPane(newPane) | |||
| 562 | - | ||||
| 563 | - | if url != "" { | |||
| 564 | - | if err := newView.LoadURL(url); err != nil { | |||
| 565 | - | log.Printf("[workspace] failed to load popup URL: %v", err) | |||
| 566 | - | } | |||
| 567 | - | if err := newView.Show(); err != nil { | |||
| 568 | - | log.Printf("[workspace] failed to show popup WebView: %v", err) | |||
| 569 | - | } | |||
| 570 | - | } | |||
| 571 | - | ||||
| 572 | - | log.Printf("[workspace] Created popup pane for URL: %s", url) | |||
| 573 | - | return newView | |||
| 574 | - | } | |||
| 575 | - | ||||
| 576 | // insertIndependentPane inserts a new independent pane next to the source | 32 | // insertIndependentPane inserts a new independent pane next to the source | ||
| 577 | func (wm *WorkspaceManager) insertIndependentPane(sourceNode *paneNode, webView *webkit.WebView, url string) error { | 33 | func (wm *WorkspaceManager) insertIndependentPane(sourceNode *paneNode, webView *webkit.WebView, url string) error { | ||
| 578 | newPane, err := wm.createPane(webView) | 34 | newPane, err := wm.createPane(webView) | ||
| @@ -803,7 +259,7 @@ func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webVi | |||||
| 803 | // Tabs are embedded in workspace paned containers - no separate window needed | 259 | // Tabs are embedded in workspace paned containers - no separate window needed | ||
| 804 | webkitCfg.CreateWindow = false | 260 | webkitCfg.CreateWindow = false | ||
| 805 | 261 | ||||
| 806 | - | // Create independent WebView like handleIntentAsTab does | 262 | + | // Create independent WebView matching our standard tab insertion path |
| 807 | independentView, err := webkit.NewWebView(webkitCfg) | 263 | independentView, err := webkit.NewWebView(webkitCfg) | ||
| 808 | if err != nil { | 264 | if err != nil { | ||
| 809 | log.Printf("[workspace] failed to create independent tab WebView: %v", err) | 265 | log.Printf("[workspace] failed to create independent tab WebView: %v", err) | ||
| @@ -824,3 +280,318 @@ func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webVi | |||||
| 824 | wm.configureRelatedPopup(sourceNode, webView, url, features) | 280 | wm.configureRelatedPopup(sourceNode, webView, url, features) | ||
| 825 | } | 281 | } | ||
| 826 | } | 282 | } | ||
| 283 | + | ||||
| 284 | + | // setupPopupHandling connects WebView's create signal for popup lifecycle management | |||
| 285 | + | // This implements the WebKit-native popup lifecycle as described in POPUP_REFACTORING_PLAN.md | |||
| 286 | + | func (wm *WorkspaceManager) setupPopupHandling(webView *webkit.WebView, parentNode *paneNode) { | |||
| 287 | + | if webView == nil || parentNode == nil { | |||
| 288 | + | return | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | webView.RegisterPopupCreateHandler(func(navAction *webkitgtk.NavigationAction) *webkit.WebView { | |||
| 292 | + | // Get URL from navigation action | |||
| 293 | + | req := navAction.Request() | |||
| 294 | + | if req == nil { | |||
| 295 | + | log.Printf("[workspace] create signal: no request in navigation action") | |||
| 296 | + | return nil | |||
| 297 | + | } | |||
| 298 | + | url := req.URI() | |||
| 299 | + | ||||
| 300 | + | log.Printf("[workspace] create signal received for URL: %s", url) | |||
| 301 | + | ||||
| 302 | + | // Check if popups are enabled in config | |||
| 303 | + | if wm.app.config != nil && !wm.app.config.Workspace.Popups.OpenInNewPane { | |||
| 304 | + | log.Printf("[workspace] Popup creation disabled in config - blocking popup") | |||
| 305 | + | return nil | |||
| 306 | + | } | |||
| 307 | + | ||||
| 308 | + | // CRITICAL: Create a BARE gotk4 WebView as RELATED to parent (no wrapping, no initialization) | |||
| 309 | + | // WebKit requires the related-view property to be set during construction for proper lifecycle | |||
| 310 | + | // This ensures session/cookie sharing and proper WindowFeatures initialization | |||
| 311 | + | // All initialization (wrapping, container, scripts) happens in ready-to-show | |||
| 312 | + | log.Printf("[workspace] Step 1: Creating bare gotk4 WebView with related-view property") | |||
| 313 | + | barePopupView := webkit.NewBareRelatedWebView(webView.GtkWebView()) | |||
| 314 | + | if barePopupView == nil { | |||
| 315 | + | log.Printf("[workspace] Failed to create bare related popup WebView - blocking popup") | |||
| 316 | + | return nil | |||
| 317 | + | } | |||
| 318 | + | log.Printf("[workspace] Step 1 complete: bare related gotk4 WebView created=%p (parent=%p)", barePopupView, webView.GtkWebView()) | |||
| 319 | + | ||||
| 320 | + | // Wrap the bare view immediately to get a proper ID | |||
| 321 | + | // BUT don't initialize it yet - that happens in ready-to-show | |||
| 322 | + | log.Printf("[workspace] Step 2: Wrapping bare view (no initialization)") | |||
| 323 | + | wrappedView := webkit.WrapBareWebView(barePopupView) | |||
| 324 | + | if wrappedView == nil { | |||
| 325 | + | log.Printf("[workspace] Failed to wrap bare popup WebView - blocking popup") | |||
| 326 | + | return nil | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | // Use the wrapper's ID for tracking | |||
| 330 | + | popupID := wrappedView.ID() | |||
| 331 | + | log.Printf("[workspace] Step 2 complete: wrapped view created with ID=%d", popupID) | |||
| 332 | + | ||||
| 333 | + | // Store wrapped view (not initialized) for initialization in ready-to-show | |||
| 334 | + | log.Printf("[workspace] Step 3: Storing in pendingPopups map") | |||
| 335 | + | wm.pendingPopups[popupID] = &pendingPopup{ | |||
| 336 | + | wrappedView: wrappedView, | |||
| 337 | + | parentView: webView, | |||
| 338 | + | parentNode: parentNode, | |||
| 339 | + | url: url, | |||
| 340 | + | } | |||
| 341 | + | log.Printf("[workspace] Step 3 complete: stored in pendingPopups") | |||
| 342 | + | ||||
| 343 | + | // CRITICAL: Do NOT connect signals before returning to WebKit | |||
| 344 | + | // Connecting signals might trigger WebKit to access WindowFeatures before it's set | |||
| 345 | + | // Instead, we'll connect them after WebKit has processed the return | |||
| 346 | + | log.Printf("[workspace] Step 4: Scheduling signal connections for next main loop iteration (AFTER WebKit configures the view)") | |||
| 347 | + | ||||
| 348 | + | // Use GLib idle_add to connect signals AFTER this callback returns | |||
| 349 | + | // This ensures WebKit has finished configuring WindowFeatures before we touch the view | |||
| 350 | + | glib.IdleAdd(func() bool { | |||
| 351 | + | log.Printf("[workspace] Idle callback: Now connecting signals for popup ID=%d", popupID) | |||
| 352 | + | ||||
| 353 | + | // Connect ready-to-show signal NOW (after WebKit has configured the view) | |||
| 354 | + | barePopupView.ConnectReadyToShow(func() { | |||
| 355 | + | log.Printf("[workspace] ready-to-show signal received for popup ID=%d", popupID) | |||
| 356 | + | wm.handlePopupReadyToShow(popupID) | |||
| 357 | + | }) | |||
| 358 | + | log.Printf("[workspace] Idle callback: ready-to-show connected") | |||
| 359 | + | ||||
| 360 | + | // Connect close handler | |||
| 361 | + | barePopupView.ConnectClose(func() { | |||
| 362 | + | log.Printf("[workspace] close signal received for popup ID=%d", popupID) | |||
| 363 | + | wm.handlePopupClose(popupID) | |||
| 364 | + | }) | |||
| 365 | + | log.Printf("[workspace] Idle callback: close signal connected") | |||
| 366 | + | ||||
| 367 | + | return false // Don't repeat | |||
| 368 | + | }) | |||
| 369 | + | ||||
| 370 | + | log.Printf("[workspace] Step 4 complete: signal connections scheduled") | |||
| 371 | + | ||||
| 372 | + | log.Printf("[workspace] Step 5: Returning wrapped view to WebKit (will extract bare view in ConnectCreate)") | |||
| 373 | + | ||||
| 374 | + | // Return the wrapped view - WebKit will extract the underlying gotk4 view and configure it | |||
| 375 | + | return wrappedView | |||
| 376 | + | }) | |||
| 377 | + | } | |||
| 378 | + | ||||
| 379 | + | // handlePopupReadyToShow inserts popup pane when WebKit says it's ready | |||
| 380 | + | func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) { | |||
| 381 | + | info, ok := wm.pendingPopups[popupID] | |||
| 382 | + | if !ok { | |||
| 383 | + | log.Printf("[workspace] ready-to-show for unknown popup ID: %d", popupID) | |||
| 384 | + | return | |||
| 385 | + | } | |||
| 386 | + | delete(wm.pendingPopups, popupID) | |||
| 387 | + | ||||
| 388 | + | log.Printf("[workspace] ready-to-show received for popup ID=%d, starting full initialization", popupID) | |||
| 389 | + | ||||
| 390 | + | // NOW it's safe to initialize (WebKit has configured WindowProperties) | |||
| 391 | + | // We already have the wrapped view from setupPopupHandling | |||
| 392 | + | wrappedView := info.wrappedView | |||
| 393 | + | ||||
| 394 | + | // Build webkit config for initialization | |||
| 395 | + | webkitCfg, err := wm.app.buildWebkitConfig() | |||
| 396 | + | if err != nil { | |||
| 397 | + | log.Printf("[workspace] Failed to build webkit config for popup initialization: %v", err) | |||
| 398 | + | return | |||
| 399 | + | } | |||
| 400 | + | webkitCfg.CreateWindow = false // Embedded in workspace | |||
| 401 | + | ||||
| 402 | + | // Initialize the wrapped view with full configuration | |||
| 403 | + | if err := wrappedView.InitializeFromBare(webkitCfg); err != nil { | |||
| 404 | + | log.Printf("[workspace] Failed to initialize popup WebView: %v", err) | |||
| 405 | + | return | |||
| 406 | + | } | |||
| 407 | + | ||||
| 408 | + | log.Printf("[workspace] Successfully initialized popup WebView ID %d", wrappedView.ID()) | |||
| 409 | + | ||||
| 410 | + | // Create pane for the fully initialized WebView | |||
| 411 | + | popupPane, err := wm.createPane(wrappedView) | |||
| 412 | + | if err != nil { | |||
| 413 | + | log.Printf("[workspace] Failed to create popup pane: %v", err) | |||
| 414 | + | return | |||
| 415 | + | } | |||
| 416 | + | ||||
| 417 | + | log.Printf("[workspace] Created popup pane, now inserting into workspace") | |||
| 418 | + | ||||
| 419 | + | // NOW it's safe to insert into workspace based on configured behavior | |||
| 420 | + | behavior := wm.app.config.Workspace.Popups.Behavior | |||
| 421 | + | log.Printf("[workspace] Popup behavior: %s", behavior) | |||
| 422 | + | ||||
| 423 | + | switch behavior { | |||
| 424 | + | case config.PopupBehaviorSplit: | |||
| 425 | + | // Split pane behavior (default) | |||
| 426 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 427 | + | if direction == "" { | |||
| 428 | + | direction = "right" | |||
| 429 | + | } | |||
| 430 | + | if err := wm.insertPopupPane(info.parentNode, popupPane, direction); err != nil { | |||
| 431 | + | log.Printf("[workspace] Failed to insert split popup pane: %v", err) | |||
| 432 | + | return | |||
| 433 | + | } | |||
| 434 | + | ||||
| 435 | + | case config.PopupBehaviorStacked: | |||
| 436 | + | // Stacked pane behavior | |||
| 437 | + | _, err := wm.stackedPaneManager.StackPane(info.parentNode) | |||
| 438 | + | if err != nil { | |||
| 439 | + | log.Printf("[workspace] Failed to stack existing pane: %v", err) | |||
| 440 | + | return | |||
| 441 | + | } | |||
| 442 | + | // Insert the popup pane in the same container | |||
| 443 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 444 | + | if direction == "" { | |||
| 445 | + | direction = "right" | |||
| 446 | + | } | |||
| 447 | + | if err := wm.insertPopupPane(info.parentNode, popupPane, direction); err != nil { | |||
| 448 | + | log.Printf("[workspace] Failed to insert stacked popup pane: %v", err) | |||
| 449 | + | return | |||
| 450 | + | } | |||
| 451 | + | ||||
| 452 | + | case config.PopupBehaviorTabbed: | |||
| 453 | + | // Tabbed behavior - TODO: implement proper tabbed interface | |||
| 454 | + | log.Printf("[workspace] WARNING: Tabbed popup behavior not yet implemented, falling back to split") | |||
| 455 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 456 | + | if direction == "" { | |||
| 457 | + | direction = "right" | |||
| 458 | + | } | |||
| 459 | + | if err := wm.insertPopupPane(info.parentNode, popupPane, direction); err != nil { | |||
| 460 | + | log.Printf("[workspace] Failed to insert tabbed popup pane: %v", err) | |||
| 461 | + | return | |||
| 462 | + | } | |||
| 463 | + | ||||
| 464 | + | case config.PopupBehaviorWindowed: | |||
| 465 | + | // Windowed behavior - opens in new workspace/window | |||
| 466 | + | // TODO: implement new window creation with GTK | |||
| 467 | + | log.Printf("[workspace] WARNING: Windowed popup behavior not yet implemented, falling back to split") | |||
| 468 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 469 | + | if direction == "" { | |||
| 470 | + | direction = "right" | |||
| 471 | + | } | |||
| 472 | + | if err := wm.insertPopupPane(info.parentNode, popupPane, direction); err != nil { | |||
| 473 | + | log.Printf("[workspace] Failed to insert windowed popup pane: %v", err) | |||
| 474 | + | return | |||
| 475 | + | } | |||
| 476 | + | ||||
| 477 | + | default: | |||
| 478 | + | // Fallback to split if unknown behavior | |||
| 479 | + | log.Printf("[workspace] Unknown popup behavior '%s', falling back to split", behavior) | |||
| 480 | + | direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement) | |||
| 481 | + | if direction == "" { | |||
| 482 | + | direction = "right" | |||
| 483 | + | } | |||
| 484 | + | if err := wm.insertPopupPane(info.parentNode, popupPane, direction); err != nil { | |||
| 485 | + | log.Printf("[workspace] Failed to insert popup pane: %v", err) | |||
| 486 | + | return | |||
| 487 | + | } | |||
| 488 | + | } | |||
| 489 | + | ||||
| 490 | + | // Configure the popup node | |||
| 491 | + | node := wm.viewToNode[wrappedView] | |||
| 492 | + | if node != nil { | |||
| 493 | + | node.windowType = webkit.WindowTypePopup | |||
| 494 | + | node.isRelated = true | |||
| 495 | + | node.parentPane = info.parentNode | |||
| 496 | + | node.isPopup = true | |||
| 497 | + | node.autoClose = wm.shouldAutoClose(info.url) | |||
| 498 | + | node.popupID = popupID // Store popupID for close signal lookup | |||
| 499 | + | ||||
| 500 | + | // Track this popup in the parent's activePopupChildren | |||
| 501 | + | if info.parentNode != nil { | |||
| 502 | + | popupWebViewID := wrappedView.IDString() | |||
| 503 | + | if info.parentNode.activePopupChildren == nil { | |||
| 504 | + | info.parentNode.activePopupChildren = make([]string, 0) | |||
| 505 | + | } | |||
| 506 | + | info.parentNode.activePopupChildren = append(info.parentNode.activePopupChildren, popupWebViewID) | |||
| 507 | + | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", | |||
| 508 | + | popupWebViewID, len(info.parentNode.activePopupChildren)) | |||
| 509 | + | ||||
| 510 | + | // Register navigation policy handler for parent | |||
| 511 | + | if info.parentNode.pane != nil && info.parentNode.pane.WebView() != nil { | |||
| 512 | + | info.parentNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |||
| 513 | + | if isUserGesture { | |||
| 514 | + | return true | |||
| 515 | + | } | |||
| 516 | + | if len(info.parentNode.activePopupChildren) > 0 { | |||
| 517 | + | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", | |||
| 518 | + | len(info.parentNode.activePopupChildren), url) | |||
| 519 | + | return false | |||
| 520 | + | } | |||
| 521 | + | return true | |||
| 522 | + | }) | |||
| 523 | + | } | |||
| 524 | + | } | |||
| 525 | + | ||||
| 526 | + | // URL-based auto-close for OAuth popups | |||
| 527 | + | if node.autoClose { | |||
| 528 | + | wm.registerOAuthAutoClose(wrappedView, info.url) | |||
| 529 | + | } | |||
| 530 | + | } | |||
| 531 | + | ||||
| 532 | + | wm.ensureGUIInPane(popupPane) | |||
| 533 | + | ||||
| 534 | + | // WebKit handles loading the URL automatically, just ensure visibility | |||
| 535 | + | if err := wrappedView.Show(); err != nil { | |||
| 536 | + | log.Printf("[workspace] failed to show popup WebView: %v", err) | |||
| 537 | + | } | |||
| 538 | + | ||||
| 539 | + | log.Printf("[workspace] Popup pane inserted and visible for URL: %s", info.url) | |||
| 540 | + | } | |||
| 541 | + | ||||
| 542 | + | // handlePopupClose responds to WebKit's close signal | |||
| 543 | + | func (wm *WorkspaceManager) handlePopupClose(popupID uint64) { | |||
| 544 | + | log.Printf("[workspace] close signal received for popup native=%d", popupID) | |||
| 545 | + | ||||
| 546 | + | // Check if it's in pending (not yet shown) | |||
| 547 | + | if info, ok := wm.pendingPopups[popupID]; ok { | |||
| 548 | + | delete(wm.pendingPopups, popupID) | |||
| 549 | + | log.Printf("[workspace] Popup closed before ready-to-show, cleaning up") | |||
| 550 | + | // Just remove from pending, WebKit will handle cleanup | |||
| 551 | + | _ = info // avoid unused variable | |||
| 552 | + | return | |||
| 553 | + | } | |||
| 554 | + | ||||
| 555 | + | // Find pane by popupID (stored in node during ready-to-show) | |||
| 556 | + | var targetNode *paneNode | |||
| 557 | + | for _, node := range wm.viewToNode { | |||
| 558 | + | if node != nil && node.popupID == popupID { | |||
| 559 | + | targetNode = node | |||
| 560 | + | break | |||
| 561 | + | } | |||
| 562 | + | } | |||
| 563 | + | ||||
| 564 | + | if targetNode == nil { | |||
| 565 | + | log.Printf("[workspace] close signal for unknown popup ID=%d, already closed?", popupID) | |||
| 566 | + | return | |||
| 567 | + | } | |||
| 568 | + | ||||
| 569 | + | // Remove from parent's activePopupChildren | |||
| 570 | + | // Find the wrapped WebView for this node to get its IDString | |||
| 571 | + | var wrappedViewID string | |||
| 572 | + | for webView, node := range wm.viewToNode { | |||
| 573 | + | if node == targetNode { | |||
| 574 | + | wrappedViewID = webView.IDString() | |||
| 575 | + | break | |||
| 576 | + | } | |||
| 577 | + | } | |||
| 578 | + | ||||
| 579 | + | if targetNode.parentPane != nil && wrappedViewID != "" { | |||
| 580 | + | for i, childID := range targetNode.parentPane.activePopupChildren { | |||
| 581 | + | if childID == wrappedViewID { | |||
| 582 | + | targetNode.parentPane.activePopupChildren = append( | |||
| 583 | + | targetNode.parentPane.activePopupChildren[:i], | |||
| 584 | + | targetNode.parentPane.activePopupChildren[i+1:]...) | |||
| 585 | + | log.Printf("[workspace] Removed popup from parent's activePopupChildren (remaining: %d)", | |||
| 586 | + | len(targetNode.parentPane.activePopupChildren)) | |||
| 587 | + | break | |||
| 588 | + | } | |||
| 589 | + | } | |||
| 590 | + | } | |||
| 591 | + | ||||
| 592 | + | // Clean close via existing ClosePane (WebKit already stopped) | |||
| 593 | + | log.Printf("[workspace] Closing popup pane via ClosePane") | |||
| 594 | + | if err := wm.ClosePane(targetNode); err != nil { | |||
| 595 | + | log.Printf("[workspace] Error closing popup pane: %v", err) | |||
| 596 | + | } | |||
| 597 | + | } | |||
M internal/app/browser/workspace_types.go +28 -17
| @@ -57,25 +57,26 @@ type paneNode struct { | |||
| 57 | 57 | isPopup bool // Deprecated: use windowType instead | |
| 58 | 58 | ||
| 59 | 59 | // Window type tracking | |
| 60 | - | windowType webkit.WindowType // Tab or Popup | |
| 61 | - | windowFeatures *webkit.WindowFeatures // Features if popup | |
| 62 | - | isRelated bool // Shares context | |
| 63 | - | parentPane *paneNode // Parent for related views | |
| 64 | - | activePopupChildren []string // WebView IDs of active popup children (for tracking related popups) | |
| 65 | - | autoClose bool // Auto-close on OAuth success | |
| 66 | - | requestID string // Request ID for popup (used for localStorage cleanup) | |
| 67 | - | hoverToken *gtk.EventControllerMotion // Hover event controller for mouse-based focus | |
| 68 | - | focusControllerToken uintptr // Focus controller token (stored as native pointer) | |
| 69 | - | pendingHoverReattach bool // Needs hover reattachment after reparent | |
| 70 | - | pendingFocusReattach bool // Needs focus reattachment after reparent | |
| 71 | - | lastHoverTime int64 // Timestamp of last hover event (for debouncing) | |
| 60 | + | windowType webkit.WindowType // Tab or Popup | |
| 61 | + | windowFeatures *webkit.WindowFeatures // Features if popup | |
| 62 | + | isRelated bool // Shares context | |
| 63 | + | parentPane *paneNode // Parent for related views | |
| 64 | + | activePopupChildren []string // WebView IDs of active popup children (for tracking related popups) | |
| 65 | + | autoClose bool // Auto-close on OAuth success | |
| 66 | + | requestID string // Request ID for popup (used for localStorage cleanup) | |
| 67 | + | popupID uint64 // Unique popup ID (for bare WebView lifecycle tracking) | |
| 68 | + | hoverToken *gtk.EventControllerMotion // Hover event controller for mouse-based focus | |
| 69 | + | focusControllerToken uintptr // Focus controller token (stored as native pointer) | |
| 70 | + | pendingHoverReattach bool // Needs hover reattachment after reparent | |
| 71 | + | pendingFocusReattach bool // Needs focus reattachment after reparent | |
| 72 | + | lastHoverTime int64 // Timestamp of last hover event (for debouncing) | |
| 72 | 73 | ||
| 73 | 74 | // Stacked panes support - terminal branch nodes | |
| 74 | - | isStacked bool // Whether this node contains stacked panes | |
| 75 | - | stackedPanes []*paneNode // List of stacked panes (if isStacked) | |
| 76 | - | activeStackIndex int // Index of currently visible pane in stack | |
| 77 | - | titleBar gtk.Widgetter // *gtk.Box for title bar (when collapsed) | |
| 78 | - | stackWrapper gtk.Widgetter // Internal *gtk.Box containing the actual stacked widgets (titles + webviews) | |
| 75 | + | isStacked bool // Whether this node contains stacked panes | |
| 76 | + | stackedPanes []*paneNode // List of stacked panes (if isStacked) | |
| 77 | + | activeStackIndex int // Index of currently visible pane in stack | |
| 78 | + | titleBar gtk.Widgetter // *gtk.Box for title bar (when collapsed) | |
| 79 | + | stackWrapper gtk.Widgetter // Internal *gtk.Box containing the actual stacked widgets (titles + webviews) | |
| 79 | 80 | ||
| 80 | 81 | // Cleanup tracking | |
| 81 | 82 | widgetValid bool // Guard flagged before GTK destruction | |
| @@ -92,5 +93,15 @@ const ( | |||
| 92 | 93 | stackContainerClass = "stacked-pane-container" | |
| 93 | 94 | ) | |
| 94 | 95 | ||
| 96 | + | // pendingPopup tracks a popup WebView that has been created by WebKit but not | |
| 97 | + | // yet fully initialized/inserted into the workspace. It allows the ready-to-show | |
| 98 | + | // handler to finish setup once WebKit has configured window properties. | |
| 99 | + | type pendingPopup struct { | |
| 100 | + | wrappedView *webkit.WebView | |
| 101 | + | parentView *webkit.WebView | |
| 102 | + | parentNode *paneNode | |
| 103 | + | url string | |
| 104 | + | } | |
| 105 | + | ||
| 95 | 106 | // Focus calculation epsilon for geometric comparisons | |
| 96 | 107 | const focusEpsilon = 1e-3 | |
| @@ -57,25 +57,26 @@ type paneNode struct { | |||||
| 57 | isPopup bool // Deprecated: use windowType instead | 57 | isPopup bool // Deprecated: use windowType instead | ||
| 58 | 58 | ||||
| 59 | // Window type tracking | 59 | // Window type tracking | ||
| 60 | - | windowType webkit.WindowType // Tab or Popup | 60 | + | windowType webkit.WindowType // Tab or Popup |
| 61 | - | windowFeatures *webkit.WindowFeatures // Features if popup | 61 | + | windowFeatures *webkit.WindowFeatures // Features if popup |
| 62 | - | isRelated bool // Shares context | 62 | + | isRelated bool // Shares context |
| 63 | - | parentPane *paneNode // Parent for related views | 63 | + | parentPane *paneNode // Parent for related views |
| 64 | - | activePopupChildren []string // WebView IDs of active popup children (for tracking related popups) | 64 | + | activePopupChildren []string // WebView IDs of active popup children (for tracking related popups) |
| 65 | - | autoClose bool // Auto-close on OAuth success | 65 | + | autoClose bool // Auto-close on OAuth success |
| 66 | - | requestID string // Request ID for popup (used for localStorage cleanup) | 66 | + | requestID string // Request ID for popup (used for localStorage cleanup) |
| 67 | - | hoverToken *gtk.EventControllerMotion // Hover event controller for mouse-based focus | 67 | + | popupID uint64 // Unique popup ID (for bare WebView lifecycle tracking) |
| 68 | - | focusControllerToken uintptr // Focus controller token (stored as native pointer) | 68 | + | hoverToken *gtk.EventControllerMotion // Hover event controller for mouse-based focus |
| 69 | - | pendingHoverReattach bool // Needs hover reattachment after reparent | 69 | + | focusControllerToken uintptr // Focus controller token (stored as native pointer) |
| 70 | - | pendingFocusReattach bool // Needs focus reattachment after reparent | 70 | + | pendingHoverReattach bool // Needs hover reattachment after reparent |
| 71 | - | lastHoverTime int64 // Timestamp of last hover event (for debouncing) | 71 | + | pendingFocusReattach bool // Needs focus reattachment after reparent |
| 72 | + | lastHoverTime int64 // Timestamp of last hover event (for debouncing) | |||
| 72 | 73 | ||||
| 73 | // Stacked panes support - terminal branch nodes | 74 | // Stacked panes support - terminal branch nodes | ||
| 74 | - | isStacked bool // Whether this node contains stacked panes | 75 | + | isStacked bool // Whether this node contains stacked panes |
| 75 | - | stackedPanes []*paneNode // List of stacked panes (if isStacked) | 76 | + | stackedPanes []*paneNode // List of stacked panes (if isStacked) |
| 76 | - | activeStackIndex int // Index of currently visible pane in stack | 77 | + | activeStackIndex int // Index of currently visible pane in stack |
| 77 | - | titleBar gtk.Widgetter // *gtk.Box for title bar (when collapsed) | 78 | + | titleBar gtk.Widgetter // *gtk.Box for title bar (when collapsed) |
| 78 | - | stackWrapper gtk.Widgetter // Internal *gtk.Box containing the actual stacked widgets (titles + webviews) | 79 | + | stackWrapper gtk.Widgetter // Internal *gtk.Box containing the actual stacked widgets (titles + webviews) |
| 79 | 80 | ||||
| 80 | // Cleanup tracking | 81 | // Cleanup tracking | ||
| 81 | widgetValid bool // Guard flagged before GTK destruction | 82 | widgetValid bool // Guard flagged before GTK destruction | ||
| @@ -92,5 +93,15 @@ const ( | |||||
| 92 | stackContainerClass = "stacked-pane-container" | 93 | stackContainerClass = "stacked-pane-container" | ||
| 93 | ) | 94 | ) | ||
| 94 | 95 | ||||
| 96 | + | // pendingPopup tracks a popup WebView that has been created by WebKit but not | |||
| 97 | + | // yet fully initialized/inserted into the workspace. It allows the ready-to-show | |||
| 98 | + | // handler to finish setup once WebKit has configured window properties. | |||
| 99 | + | type pendingPopup struct { | |||
| 100 | + | wrappedView *webkit.WebView | |||
| 101 | + | parentView *webkit.WebView | |||
| 102 | + | parentNode *paneNode | |||
| 103 | + | url string | |||
| 104 | + | } | |||
| 105 | + | ||||
| 95 | // Focus calculation epsilon for geometric comparisons | 106 | // Focus calculation epsilon for geometric comparisons | ||
| 96 | const focusEpsilon = 1e-3 | 107 | const focusEpsilon = 1e-3 | ||