fix(popup): prevent OAuth hijacking, fix rendering corruption, and clean up localStorage
- Add navigation policy handler to all popup types to block script-initiated navigations in parent panes when popups are active (prevents OAuth window.opener hijacking) - Apply hide→show→resize→draw sequence to parent pane after popup closes to fix GTK rendering corruption - Clean up all popup-related localStorage entries (popup_*_parent_info, popup_*_parent_action, etc.) when popup closes - Add requestID tracking to paneNode for proper localStorage cleanup - Add RegisterNavigationPolicyHandler to both CGO and stub WebView implementations - Remove obsolete modules/window-open.ts module (window.open now handled in main-world.ts)
- commit
- a06c3904455f1e99dcffde6d994038d48b664ffb
- author
- Brice · · 2025-10-04 08:57 UTC
- parent
- d7d1748
- signature
- unsigned
12 files · +539 -59
M gui/src/injected/gui.ts +1 -4
| @@ -13,7 +13,6 @@ import { | |||
| 13 | 13 | type WorkspaceConfigPayload, | |
| 14 | 14 | type WorkspaceRuntime, | |
| 15 | 15 | } from "./modules/workspace"; | |
| 16 | - | import { initializeWindowOpenInterceptor } from "./modules/window-open"; | |
| 17 | 16 | import { keyboardService, type KeyboardService } from "$lib/keyboard"; | |
| 18 | 17 | import type { Suggestion } from "../components/omnibox/types"; | |
| 19 | 18 | // Note: color-scheme module is loaded separately at document-start by WebKit | |
| @@ -156,9 +155,7 @@ if (!window.__dumber_gui_ready) { | |||
| 156 | 155 | whenDOMReady(async () => { | |
| 157 | 156 | try { | |
| 158 | 157 | // Page-world bridge is injected by WebKit; avoid duplicate injection here | |
| 159 | - | ||
| 160 | - | // Initialize window.open interceptor first (must be early) | |
| 161 | - | initializeWindowOpenInterceptor(); | |
| 158 | + | // window.open interceptor is now in main-world.ts (page-world context) | |
| 162 | 159 | ||
| 163 | 160 | // Initialize toast system first | |
| 164 | 161 | await initializeToast(); | |
| @@ -13,7 +13,6 @@ import { | |||||
| 13 | type WorkspaceConfigPayload, | 13 | type WorkspaceConfigPayload, | ||
| 14 | type WorkspaceRuntime, | 14 | type WorkspaceRuntime, | ||
| 15 | } from "./modules/workspace"; | 15 | } from "./modules/workspace"; | ||
| 16 | - | import { initializeWindowOpenInterceptor } from "./modules/window-open"; | |||
| 17 | import { keyboardService, type KeyboardService } from "$lib/keyboard"; | 16 | import { keyboardService, type KeyboardService } from "$lib/keyboard"; | ||
| 18 | import type { Suggestion } from "../components/omnibox/types"; | 17 | import type { Suggestion } from "../components/omnibox/types"; | ||
| 19 | // Note: color-scheme module is loaded separately at document-start by WebKit | 18 | // Note: color-scheme module is loaded separately at document-start by WebKit | ||
| @@ -156,9 +155,7 @@ if (!window.__dumber_gui_ready) { | |||||
| 156 | whenDOMReady(async () => { | 155 | whenDOMReady(async () => { | ||
| 157 | try { | 156 | try { | ||
| 158 | // Page-world bridge is injected by WebKit; avoid duplicate injection here | 157 | // Page-world bridge is injected by WebKit; avoid duplicate injection here | ||
| 159 | - | 158 | + | // window.open interceptor is now in main-world.ts (page-world context) | |
| 160 | - | // Initialize window.open interceptor first (must be early) | |||
| 161 | - | initializeWindowOpenInterceptor(); | |||
| 162 | 159 | ||||
| 163 | // Initialize toast system first | 160 | // Initialize toast system first | ||
| 164 | await initializeToast(); | 161 | await initializeToast(); | ||
D gui/src/injected/modules/window-open.ts +0 -29
| @@ -1,29 +0,0 @@ | |||
| 1 | - | /** | |
| 2 | - | * Window Open Interceptor Module | |
| 3 | - | * | |
| 4 | - | * OBSOLETE: window.open() is now handled directly via page-world bridge bypass. | |
| 5 | - | * This module is kept for compatibility but no longer performs active interception. | |
| 6 | - | */ | |
| 7 | - | ||
| 8 | - | let isInitialized = false; | |
| 9 | - | ||
| 10 | - | /** | |
| 11 | - | * Initialize the window.open interceptor (no-op) | |
| 12 | - | * Window.open is now handled directly via JavaScript bypass in page-world | |
| 13 | - | */ | |
| 14 | - | export function initializeWindowOpenInterceptor(): void { | |
| 15 | - | if (isInitialized) { | |
| 16 | - | return; | |
| 17 | - | } | |
| 18 | - | ||
| 19 | - | // No-op - window.open handled directly via page-world bridge | |
| 20 | - | isInitialized = true; | |
| 21 | - | console.log("[window-open] ✅ Interceptor initialized (bypass mode)"); | |
| 22 | - | } | |
| 23 | - | ||
| 24 | - | /** | |
| 25 | - | * Get initialization status | |
| 26 | - | */ | |
| 27 | - | export function isWindowOpenInterceptorInitialized(): boolean { | |
| 28 | - | return isInitialized; | |
| 29 | - | } | |
| @@ -1,29 +0,0 @@ | |||||
| 1 | - | /** | |||
| 2 | - | * Window Open Interceptor Module | |||
| 3 | - | * | |||
| 4 | - | * OBSOLETE: window.open() is now handled directly via page-world bridge bypass. | |||
| 5 | - | * This module is kept for compatibility but no longer performs active interception. | |||
| 6 | - | */ | |||
| 7 | - | ||||
| 8 | - | let isInitialized = false; | |||
| 9 | - | ||||
| 10 | - | /** | |||
| 11 | - | * Initialize the window.open interceptor (no-op) | |||
| 12 | - | * Window.open is now handled directly via JavaScript bypass in page-world | |||
| 13 | - | */ | |||
| 14 | - | export function initializeWindowOpenInterceptor(): void { | |||
| 15 | - | if (isInitialized) { | |||
| 16 | - | return; | |||
| 17 | - | } | |||
| 18 | - | ||||
| 19 | - | // No-op - window.open handled directly via page-world bridge | |||
| 20 | - | isInitialized = true; | |||
| 21 | - | console.log("[window-open] ✅ Interceptor initialized (bypass mode)"); | |||
| 22 | - | } | |||
| 23 | - | ||||
| 24 | - | /** | |||
| 25 | - | * Get initialization status | |||
| 26 | - | */ | |||
| 27 | - | export function isWindowOpenInterceptorInitialized(): boolean { | |||
| 28 | - | return isInitialized; | |||
| 29 | - | } | |||
M internal/app/browser/workspace_manager.go +89 -0
| @@ -397,11 +397,100 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa | |||
| 397 | 397 | ||
| 398 | 398 | log.Printf("[workspace] Closing popup pane due to %s", msg.Reason) | |
| 399 | 399 | ||
| 400 | + | // Remove this popup from parent's activePopupChildren before closing | |
| 401 | + | if targetNode.parentPane != nil { | |
| 402 | + | parentNode := targetNode.parentPane | |
| 403 | + | for i, childID := range parentNode.activePopupChildren { | |
| 404 | + | if childID == msg.WebViewID { | |
| 405 | + | parentNode.activePopupChildren = append(parentNode.activePopupChildren[:i], parentNode.activePopupChildren[i+1:]...) | |
| 406 | + | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", msg.WebViewID, len(parentNode.activePopupChildren)) | |
| 407 | + | break | |
| 408 | + | } | |
| 409 | + | } | |
| 410 | + | ||
| 411 | + | // Clean up ALL localStorage entries for this popup in parent WebView | |
| 412 | + | // This includes popup_<requestId>_parent_info, popup_<requestId>_parent_action, etc. | |
| 413 | + | if parentNode.pane != nil && parentNode.pane.WebView() != nil && targetNode.requestID != "" { | |
| 414 | + | parentWebViewID := parentNode.pane.WebView().ID() | |
| 415 | + | requestID := targetNode.requestID | |
| 416 | + | ||
| 417 | + | cleanupScript := fmt.Sprintf(` | |
| 418 | + | try { | |
| 419 | + | // Clean up popup_mapping_<parentId> (from Go code) | |
| 420 | + | localStorage.removeItem('popup_mapping_%s'); | |
| 421 | + | ||
| 422 | + | // Clean up popup_<requestId>_* entries (from JavaScript) | |
| 423 | + | const requestId = '%s'; | |
| 424 | + | const keysToRemove = [ | |
| 425 | + | 'popup_' + requestId + '_parent_info', | |
| 426 | + | 'popup_' + requestId + '_parent_action', | |
| 427 | + | 'popup_' + requestId + '_message_to_popup', | |
| 428 | + | 'popup_' + requestId + '_message_to_parent', | |
| 429 | + | 'oauth_callback_' + '%s' | |
| 430 | + | ]; | |
| 431 | + | ||
| 432 | + | keysToRemove.forEach(key => { | |
| 433 | + | try { | |
| 434 | + | localStorage.removeItem(key); | |
| 435 | + | } catch(e) {} | |
| 436 | + | }); | |
| 437 | + | ||
| 438 | + | console.log('[workspace] Cleaned up popup localStorage for requestId:', requestId); | |
| 439 | + | } catch(e) { | |
| 440 | + | console.warn('[workspace] Failed to clean popup localStorage:', e); | |
| 441 | + | } | |
| 442 | + | `, parentWebViewID, requestID, msg.WebViewID) | |
| 443 | + | ||
| 444 | + | if err := parentNode.pane.WebView().InjectScript(cleanupScript); err != nil { | |
| 445 | + | log.Printf("[workspace] Failed to inject localStorage cleanup script: %v", err) | |
| 446 | + | } else { | |
| 447 | + | log.Printf("[workspace] Cleaned up localStorage for popup requestId=%s webviewId=%s", requestID, msg.WebViewID) | |
| 448 | + | } | |
| 449 | + | } | |
| 450 | + | } | |
| 451 | + | ||
| 400 | 452 | // Close the popup pane | |
| 401 | 453 | if err := wm.ClosePane(targetNode); err != nil { | |
| 402 | 454 | log.Printf("[workspace] Failed to close popup pane: %v", err) | |
| 403 | 455 | } else { | |
| 404 | 456 | log.Printf("[workspace] Successfully closed popup pane: %s", msg.WebViewID) | |
| 457 | + | ||
| 458 | + | // Fix GTK rendering for parent pane after popup closes | |
| 459 | + | // The parent pane's rendering can become corrupted after popup closes | |
| 460 | + | if targetNode.parentPane != nil { | |
| 461 | + | parentNode := targetNode.parentPane | |
| 462 | + | if parentNode.container != 0 && webkit.WidgetIsValid(parentNode.container) { | |
| 463 | + | log.Printf("[workspace] Fixing parent pane rendering after popup close") | |
| 464 | + | ||
| 465 | + | // CRITICAL: Hide parent container to disconnect WebKitGTK rendering pipeline | |
| 466 | + | webkit.WidgetHide(parentNode.container) | |
| 467 | + | ||
| 468 | + | // Schedule showing and forcing GTK to reconnect rendering | |
| 469 | + | wm.scheduleIdleGuarded(func() bool { | |
| 470 | + | if parentNode == nil || !parentNode.widgetValid || parentNode.container == 0 { | |
| 471 | + | return false | |
| 472 | + | } | |
| 473 | + | // Show widget to reconnect WebKit rendering pipeline | |
| 474 | + | webkit.WidgetShow(parentNode.container) | |
| 475 | + | // Force GTK to recalculate size and recreate rendering surface | |
| 476 | + | webkit.WidgetQueueResize(parentNode.container) | |
| 477 | + | webkit.WidgetQueueDraw(parentNode.container) | |
| 478 | + | ||
| 479 | + | // Also queue resize+draw on the WebView widget itself | |
| 480 | + | if parentNode.pane != nil && parentNode.pane.WebView() != nil { | |
| 481 | + | webViewWidget := parentNode.pane.WebView().Widget() | |
| 482 | + | if webViewWidget != 0 && webkit.WidgetIsValid(webViewWidget) { | |
| 483 | + | webkit.WidgetQueueResize(webViewWidget) | |
| 484 | + | webkit.WidgetQueueDraw(webViewWidget) | |
| 485 | + | log.Printf("[workspace] Queued resize+draw for parent WebView widget") | |
| 486 | + | } | |
| 487 | + | } | |
| 488 | + | ||
| 489 | + | log.Printf("[workspace] Shown and queued resize+draw for parent pane after popup close") | |
| 490 | + | return false | |
| 491 | + | }, parentNode) | |
| 492 | + | } | |
| 493 | + | } | |
| 405 | 494 | } | |
| 406 | 495 | default: | |
| 407 | 496 | log.Printf("[workspace] unhandled workspace event: %s", msg.Event) | |
| @@ -397,11 +397,100 @@ func (wm *WorkspaceManager) OnWorkspaceMessage(source *webkit.WebView, msg messa | |||||
| 397 | 397 | ||||
| 398 | log.Printf("[workspace] Closing popup pane due to %s", msg.Reason) | 398 | log.Printf("[workspace] Closing popup pane due to %s", msg.Reason) | ||
| 399 | 399 | ||||
| 400 | + | // Remove this popup from parent's activePopupChildren before closing | |||
| 401 | + | if targetNode.parentPane != nil { | |||
| 402 | + | parentNode := targetNode.parentPane | |||
| 403 | + | for i, childID := range parentNode.activePopupChildren { | |||
| 404 | + | if childID == msg.WebViewID { | |||
| 405 | + | parentNode.activePopupChildren = append(parentNode.activePopupChildren[:i], parentNode.activePopupChildren[i+1:]...) | |||
| 406 | + | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", msg.WebViewID, len(parentNode.activePopupChildren)) | |||
| 407 | + | break | |||
| 408 | + | } | |||
| 409 | + | } | |||
| 410 | + | ||||
| 411 | + | // Clean up ALL localStorage entries for this popup in parent WebView | |||
| 412 | + | // This includes popup_<requestId>_parent_info, popup_<requestId>_parent_action, etc. | |||
| 413 | + | if parentNode.pane != nil && parentNode.pane.WebView() != nil && targetNode.requestID != "" { | |||
| 414 | + | parentWebViewID := parentNode.pane.WebView().ID() | |||
| 415 | + | requestID := targetNode.requestID | |||
| 416 | + | ||||
| 417 | + | cleanupScript := fmt.Sprintf(` | |||
| 418 | + | try { | |||
| 419 | + | // Clean up popup_mapping_<parentId> (from Go code) | |||
| 420 | + | localStorage.removeItem('popup_mapping_%s'); | |||
| 421 | + | ||||
| 422 | + | // Clean up popup_<requestId>_* entries (from JavaScript) | |||
| 423 | + | const requestId = '%s'; | |||
| 424 | + | const keysToRemove = [ | |||
| 425 | + | 'popup_' + requestId + '_parent_info', | |||
| 426 | + | 'popup_' + requestId + '_parent_action', | |||
| 427 | + | 'popup_' + requestId + '_message_to_popup', | |||
| 428 | + | 'popup_' + requestId + '_message_to_parent', | |||
| 429 | + | 'oauth_callback_' + '%s' | |||
| 430 | + | ]; | |||
| 431 | + | ||||
| 432 | + | keysToRemove.forEach(key => { | |||
| 433 | + | try { | |||
| 434 | + | localStorage.removeItem(key); | |||
| 435 | + | } catch(e) {} | |||
| 436 | + | }); | |||
| 437 | + | ||||
| 438 | + | console.log('[workspace] Cleaned up popup localStorage for requestId:', requestId); | |||
| 439 | + | } catch(e) { | |||
| 440 | + | console.warn('[workspace] Failed to clean popup localStorage:', e); | |||
| 441 | + | } | |||
| 442 | + | `, parentWebViewID, requestID, msg.WebViewID) | |||
| 443 | + | ||||
| 444 | + | if err := parentNode.pane.WebView().InjectScript(cleanupScript); err != nil { | |||
| 445 | + | log.Printf("[workspace] Failed to inject localStorage cleanup script: %v", err) | |||
| 446 | + | } else { | |||
| 447 | + | log.Printf("[workspace] Cleaned up localStorage for popup requestId=%s webviewId=%s", requestID, msg.WebViewID) | |||
| 448 | + | } | |||
| 449 | + | } | |||
| 450 | + | } | |||
| 451 | + | ||||
| 400 | // Close the popup pane | 452 | // Close the popup pane | ||
| 401 | if err := wm.ClosePane(targetNode); err != nil { | 453 | if err := wm.ClosePane(targetNode); err != nil { | ||
| 402 | log.Printf("[workspace] Failed to close popup pane: %v", err) | 454 | log.Printf("[workspace] Failed to close popup pane: %v", err) | ||
| 403 | } else { | 455 | } else { | ||
| 404 | log.Printf("[workspace] Successfully closed popup pane: %s", msg.WebViewID) | 456 | log.Printf("[workspace] Successfully closed popup pane: %s", msg.WebViewID) | ||
| 457 | + | ||||
| 458 | + | // Fix GTK rendering for parent pane after popup closes | |||
| 459 | + | // The parent pane's rendering can become corrupted after popup closes | |||
| 460 | + | if targetNode.parentPane != nil { | |||
| 461 | + | parentNode := targetNode.parentPane | |||
| 462 | + | if parentNode.container != 0 && webkit.WidgetIsValid(parentNode.container) { | |||
| 463 | + | log.Printf("[workspace] Fixing parent pane rendering after popup close") | |||
| 464 | + | ||||
| 465 | + | // CRITICAL: Hide parent container to disconnect WebKitGTK rendering pipeline | |||
| 466 | + | webkit.WidgetHide(parentNode.container) | |||
| 467 | + | ||||
| 468 | + | // Schedule showing and forcing GTK to reconnect rendering | |||
| 469 | + | wm.scheduleIdleGuarded(func() bool { | |||
| 470 | + | if parentNode == nil || !parentNode.widgetValid || parentNode.container == 0 { | |||
| 471 | + | return false | |||
| 472 | + | } | |||
| 473 | + | // Show widget to reconnect WebKit rendering pipeline | |||
| 474 | + | webkit.WidgetShow(parentNode.container) | |||
| 475 | + | // Force GTK to recalculate size and recreate rendering surface | |||
| 476 | + | webkit.WidgetQueueResize(parentNode.container) | |||
| 477 | + | webkit.WidgetQueueDraw(parentNode.container) | |||
| 478 | + | ||||
| 479 | + | // Also queue resize+draw on the WebView widget itself | |||
| 480 | + | if parentNode.pane != nil && parentNode.pane.WebView() != nil { | |||
| 481 | + | webViewWidget := parentNode.pane.WebView().Widget() | |||
| 482 | + | if webViewWidget != 0 && webkit.WidgetIsValid(webViewWidget) { | |||
| 483 | + | webkit.WidgetQueueResize(webViewWidget) | |||
| 484 | + | webkit.WidgetQueueDraw(webViewWidget) | |||
| 485 | + | log.Printf("[workspace] Queued resize+draw for parent WebView widget") | |||
| 486 | + | } | |||
| 487 | + | } | |||
| 488 | + | ||||
| 489 | + | log.Printf("[workspace] Shown and queued resize+draw for parent pane after popup close") | |||
| 490 | + | return false | |||
| 491 | + | }, parentNode) | |||
| 492 | + | } | |||
| 493 | + | } | |||
| 405 | } | 494 | } | ||
| 406 | default: | 495 | default: | ||
| 407 | log.Printf("[workspace] unhandled workspace event: %s", msg.Event) | 496 | log.Printf("[workspace] unhandled workspace event: %s", msg.Event) | ||
M internal/app/browser/workspace_pane_ops.go +116 -3
| @@ -341,8 +341,10 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 341 | 341 | } | |
| 342 | 342 | webkit.WidgetSetHExpand(paned, true) | |
| 343 | 343 | webkit.WidgetSetVExpand(paned, true) | |
| 344 | - | webkit.PanedSetResizeStart(paned, true) | |
| 345 | - | webkit.PanedSetResizeEnd(paned, true) | |
| 344 | + | // For popup panes: preserve existing pane size, let popup take remaining space | |
| 345 | + | webkit.PanedSetResizeStart(paned, false) // existing pane keeps its size | |
| 346 | + | webkit.PanedSetResizeEnd(paned, true) // popup can resize | |
| 347 | + | log.Printf("[workspace] configured paned for popup: start=fixed, end=flexible") | |
| 346 | 348 | ||
| 347 | 349 | newLeaf := &paneNode{ | |
| 348 | 350 | pane: newPane, | |
| @@ -359,17 +361,44 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 359 | 361 | ||
| 360 | 362 | parent := split.parent | |
| 361 | 363 | ||
| 364 | + | // CRITICAL: Capture parent paned's divider position BEFORE reparenting to preserve layout | |
| 365 | + | var parentDividerPos int | |
| 366 | + | if parent != nil && parent.container != 0 && webkit.WidgetIsValid(parent.container) { | |
| 367 | + | parentDividerPos = webkit.PanedGetPosition(parent.container) | |
| 368 | + | log.Printf("[workspace] captured parent divider position: %d", parentDividerPos) | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | // Clear CSS classes before reparenting to avoid GTK bloom filter corruption | |
| 372 | + | existingContainer := target.container | |
| 373 | + | if existingContainer != 0 && webkit.WidgetIsValid(existingContainer) { | |
| 374 | + | // Remove active border class if present (prevents GTK bloom filter corruption during unparent) | |
| 375 | + | if webkit.WidgetHasCSSClass(existingContainer, activePaneClass) { | |
| 376 | + | webkit.WidgetRemoveCSSClass(existingContainer, activePaneClass) | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | // CRITICAL: Hide widget before reparenting to disconnect WebKitGTK rendering pipeline | |
| 380 | + | // This forces WebKit to detach its compositor, preventing rendering corruption | |
| 381 | + | webkit.WidgetHide(existingContainer) | |
| 382 | + | log.Printf("[workspace] Hidden existing container before reparenting: %#x", existingContainer) | |
| 383 | + | } | |
| 384 | + | ||
| 362 | 385 | // Prevent GTK from auto-removing controllers while the widget is temporarily unparented. | |
| 363 | 386 | wm.safelyDetachControllersBeforeReparent(target) | |
| 364 | 387 | ||
| 365 | 388 | // Detach existing container from its current GTK parent before inserting into new paned. | |
| 366 | - | existingContainer := target.container | |
| 367 | 389 | if parent == nil { | |
| 368 | 390 | // Target is the root - remove it from the window | |
| 369 | 391 | log.Printf("[workspace] removing existing container=%#x from window", existingContainer) | |
| 370 | 392 | if wm.window != nil { | |
| 371 | 393 | wm.window.SetChild(0) | |
| 372 | 394 | } | |
| 395 | + | // For root widgets, manually unparent if they have a GTK parent (like splitNode does) | |
| 396 | + | if webkit.WidgetIsValid(existingContainer) { | |
| 397 | + | if webkit.WidgetGetParent(existingContainer) != 0 { | |
| 398 | + | webkit.WidgetUnparent(existingContainer) | |
| 399 | + | log.Printf("[workspace] manually unparented root widget: %#x", existingContainer) | |
| 400 | + | } | |
| 401 | + | } | |
| 373 | 402 | } else if parent.container != 0 { | |
| 374 | 403 | // Target has a parent paned - unparent it from there | |
| 375 | 404 | log.Printf("[workspace] unparenting existing container from parent paned") | |
| @@ -422,11 +451,56 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||
| 422 | 451 | webkit.WidgetQueueAllocate(parent.container) | |
| 423 | 452 | webkit.WidgetQueueAllocate(paned) | |
| 424 | 453 | log.Printf("[workspace] paned inserted into parent successfully") | |
| 454 | + | ||
| 455 | + | // CRITICAL: Restore parent paned's divider position to preserve existing layout | |
| 456 | + | if parentDividerPos > 0 { | |
| 457 | + | webkit.PanedSetPosition(parent.container, parentDividerPos) | |
| 458 | + | log.Printf("[workspace] restored parent divider position: %d", parentDividerPos) | |
| 459 | + | } | |
| 425 | 460 | } | |
| 426 | 461 | } | |
| 427 | 462 | ||
| 428 | 463 | webkit.WidgetShow(paned) | |
| 429 | 464 | ||
| 465 | + | // CRITICAL: Set initial 50/50 split position after showing paned | |
| 466 | + | // GTK needs the widget to be realized before we can set position based on allocation | |
| 467 | + | // Schedule this to run after GTK has allocated space to the paned | |
| 468 | + | wm.scheduleIdleGuarded(func() bool { | |
| 469 | + | if !webkit.WidgetIsValid(paned) { | |
| 470 | + | return false | |
| 471 | + | } | |
| 472 | + | // Get paned allocation to calculate 50% position | |
| 473 | + | alloc := webkit.WidgetGetAllocation(paned) | |
| 474 | + | var splitPos int | |
| 475 | + | if orientation == webkit.OrientationHorizontal { | |
| 476 | + | splitPos = alloc.Width / 2 | |
| 477 | + | } else { | |
| 478 | + | splitPos = alloc.Height / 2 | |
| 479 | + | } | |
| 480 | + | if splitPos > 0 { | |
| 481 | + | webkit.PanedSetPosition(paned, splitPos) | |
| 482 | + | log.Printf("[workspace] Set initial paned position to 50%%: %d (orientation=%d, size=%dx%d)", splitPos, orientation, alloc.Width, alloc.Height) | |
| 483 | + | } | |
| 484 | + | return false | |
| 485 | + | }, split) | |
| 486 | + | ||
| 487 | + | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | |
| 488 | + | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | |
| 489 | + | if target != nil && target.container != 0 { | |
| 490 | + | wm.scheduleIdleGuarded(func() bool { | |
| 491 | + | if target == nil || !target.widgetValid || target.container == 0 { | |
| 492 | + | return false | |
| 493 | + | } | |
| 494 | + | // Show widget to reconnect WebKit rendering pipeline | |
| 495 | + | webkit.WidgetShow(target.container) | |
| 496 | + | // Force GTK to recalculate size and recreate rendering surface | |
| 497 | + | webkit.WidgetQueueResize(target.container) | |
| 498 | + | webkit.WidgetQueueDraw(target.container) | |
| 499 | + | log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting") | |
| 500 | + | return false | |
| 501 | + | }, target) | |
| 502 | + | } | |
| 503 | + | ||
| 430 | 504 | wm.viewToNode[newPane.webView] = newLeaf | |
| 431 | 505 | wm.ensureHover(newLeaf) | |
| 432 | 506 | wm.ensureHover(target) | |
| @@ -684,6 +758,45 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||
| 684 | 758 | ||
| 685 | 759 | webkit.WidgetShow(paned) | |
| 686 | 760 | ||
| 761 | + | // CRITICAL: Set initial 50/50 split position after showing paned | |
| 762 | + | // GTK needs the widget to be realized before we can set position based on allocation | |
| 763 | + | // Schedule this to run after GTK has allocated space to the paned | |
| 764 | + | wm.scheduleIdleGuarded(func() bool { | |
| 765 | + | if !webkit.WidgetIsValid(paned) { | |
| 766 | + | return false | |
| 767 | + | } | |
| 768 | + | // Get paned allocation to calculate 50% position | |
| 769 | + | alloc := webkit.WidgetGetAllocation(paned) | |
| 770 | + | var splitPos int | |
| 771 | + | if orientation == webkit.OrientationHorizontal { | |
| 772 | + | splitPos = alloc.Width / 2 | |
| 773 | + | } else { | |
| 774 | + | splitPos = alloc.Height / 2 | |
| 775 | + | } | |
| 776 | + | if splitPos > 0 { | |
| 777 | + | webkit.PanedSetPosition(paned, splitPos) | |
| 778 | + | log.Printf("[workspace] Set initial paned position to 50%%: %d (orientation=%d, size=%dx%d)", splitPos, orientation, alloc.Width, alloc.Height) | |
| 779 | + | } | |
| 780 | + | return false | |
| 781 | + | }, split) | |
| 782 | + | ||
| 783 | + | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | |
| 784 | + | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | |
| 785 | + | if target != nil && target.container != 0 { | |
| 786 | + | wm.scheduleIdleGuarded(func() bool { | |
| 787 | + | if target == nil || !target.widgetValid || target.container == 0 { | |
| 788 | + | return false | |
| 789 | + | } | |
| 790 | + | // Show widget to reconnect WebKit rendering pipeline | |
| 791 | + | webkit.WidgetShow(target.container) | |
| 792 | + | // Force GTK to recalculate size and recreate rendering surface | |
| 793 | + | webkit.WidgetQueueResize(target.container) | |
| 794 | + | webkit.WidgetQueueDraw(target.container) | |
| 795 | + | log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting") | |
| 796 | + | return false | |
| 797 | + | }, target) | |
| 798 | + | } | |
| 799 | + | ||
| 687 | 800 | wm.viewToNode[newPane.webView] = newLeaf | |
| 688 | 801 | wm.ensureHover(newLeaf) | |
| 689 | 802 | ||
| @@ -341,8 +341,10 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 341 | } | 341 | } | ||
| 342 | webkit.WidgetSetHExpand(paned, true) | 342 | webkit.WidgetSetHExpand(paned, true) | ||
| 343 | webkit.WidgetSetVExpand(paned, true) | 343 | webkit.WidgetSetVExpand(paned, true) | ||
| 344 | - | webkit.PanedSetResizeStart(paned, true) | 344 | + | // For popup panes: preserve existing pane size, let popup take remaining space |
| 345 | - | webkit.PanedSetResizeEnd(paned, true) | 345 | + | webkit.PanedSetResizeStart(paned, false) // existing pane keeps its size |
| 346 | + | webkit.PanedSetResizeEnd(paned, true) // popup can resize | |||
| 347 | + | log.Printf("[workspace] configured paned for popup: start=fixed, end=flexible") | |||
| 346 | 348 | ||||
| 347 | newLeaf := &paneNode{ | 349 | newLeaf := &paneNode{ | ||
| 348 | pane: newPane, | 350 | pane: newPane, | ||
| @@ -359,17 +361,44 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 359 | 361 | ||||
| 360 | parent := split.parent | 362 | parent := split.parent | ||
| 361 | 363 | ||||
| 364 | + | // CRITICAL: Capture parent paned's divider position BEFORE reparenting to preserve layout | |||
| 365 | + | var parentDividerPos int | |||
| 366 | + | if parent != nil && parent.container != 0 && webkit.WidgetIsValid(parent.container) { | |||
| 367 | + | parentDividerPos = webkit.PanedGetPosition(parent.container) | |||
| 368 | + | log.Printf("[workspace] captured parent divider position: %d", parentDividerPos) | |||
| 369 | + | } | |||
| 370 | + | ||||
| 371 | + | // Clear CSS classes before reparenting to avoid GTK bloom filter corruption | |||
| 372 | + | existingContainer := target.container | |||
| 373 | + | if existingContainer != 0 && webkit.WidgetIsValid(existingContainer) { | |||
| 374 | + | // Remove active border class if present (prevents GTK bloom filter corruption during unparent) | |||
| 375 | + | if webkit.WidgetHasCSSClass(existingContainer, activePaneClass) { | |||
| 376 | + | webkit.WidgetRemoveCSSClass(existingContainer, activePaneClass) | |||
| 377 | + | } | |||
| 378 | + | ||||
| 379 | + | // CRITICAL: Hide widget before reparenting to disconnect WebKitGTK rendering pipeline | |||
| 380 | + | // This forces WebKit to detach its compositor, preventing rendering corruption | |||
| 381 | + | webkit.WidgetHide(existingContainer) | |||
| 382 | + | log.Printf("[workspace] Hidden existing container before reparenting: %#x", existingContainer) | |||
| 383 | + | } | |||
| 384 | + | ||||
| 362 | // Prevent GTK from auto-removing controllers while the widget is temporarily unparented. | 385 | // Prevent GTK from auto-removing controllers while the widget is temporarily unparented. | ||
| 363 | wm.safelyDetachControllersBeforeReparent(target) | 386 | wm.safelyDetachControllersBeforeReparent(target) | ||
| 364 | 387 | ||||
| 365 | // Detach existing container from its current GTK parent before inserting into new paned. | 388 | // Detach existing container from its current GTK parent before inserting into new paned. | ||
| 366 | - | existingContainer := target.container | |||
| 367 | if parent == nil { | 389 | if parent == nil { | ||
| 368 | // Target is the root - remove it from the window | 390 | // Target is the root - remove it from the window | ||
| 369 | log.Printf("[workspace] removing existing container=%#x from window", existingContainer) | 391 | log.Printf("[workspace] removing existing container=%#x from window", existingContainer) | ||
| 370 | if wm.window != nil { | 392 | if wm.window != nil { | ||
| 371 | wm.window.SetChild(0) | 393 | wm.window.SetChild(0) | ||
| 372 | } | 394 | } | ||
| 395 | + | // For root widgets, manually unparent if they have a GTK parent (like splitNode does) | |||
| 396 | + | if webkit.WidgetIsValid(existingContainer) { | |||
| 397 | + | if webkit.WidgetGetParent(existingContainer) != 0 { | |||
| 398 | + | webkit.WidgetUnparent(existingContainer) | |||
| 399 | + | log.Printf("[workspace] manually unparented root widget: %#x", existingContainer) | |||
| 400 | + | } | |||
| 401 | + | } | |||
| 373 | } else if parent.container != 0 { | 402 | } else if parent.container != 0 { | ||
| 374 | // Target has a parent paned - unparent it from there | 403 | // Target has a parent paned - unparent it from there | ||
| 375 | log.Printf("[workspace] unparenting existing container from parent paned") | 404 | log.Printf("[workspace] unparenting existing container from parent paned") | ||
| @@ -422,11 +451,56 @@ func (wm *WorkspaceManager) insertPopupPane(target *paneNode, newPane *BrowserPa | |||||
| 422 | webkit.WidgetQueueAllocate(parent.container) | 451 | webkit.WidgetQueueAllocate(parent.container) | ||
| 423 | webkit.WidgetQueueAllocate(paned) | 452 | webkit.WidgetQueueAllocate(paned) | ||
| 424 | log.Printf("[workspace] paned inserted into parent successfully") | 453 | log.Printf("[workspace] paned inserted into parent successfully") | ||
| 454 | + | ||||
| 455 | + | // CRITICAL: Restore parent paned's divider position to preserve existing layout | |||
| 456 | + | if parentDividerPos > 0 { | |||
| 457 | + | webkit.PanedSetPosition(parent.container, parentDividerPos) | |||
| 458 | + | log.Printf("[workspace] restored parent divider position: %d", parentDividerPos) | |||
| 459 | + | } | |||
| 425 | } | 460 | } | ||
| 426 | } | 461 | } | ||
| 427 | 462 | ||||
| 428 | webkit.WidgetShow(paned) | 463 | webkit.WidgetShow(paned) | ||
| 429 | 464 | ||||
| 465 | + | // CRITICAL: Set initial 50/50 split position after showing paned | |||
| 466 | + | // GTK needs the widget to be realized before we can set position based on allocation | |||
| 467 | + | // Schedule this to run after GTK has allocated space to the paned | |||
| 468 | + | wm.scheduleIdleGuarded(func() bool { | |||
| 469 | + | if !webkit.WidgetIsValid(paned) { | |||
| 470 | + | return false | |||
| 471 | + | } | |||
| 472 | + | // Get paned allocation to calculate 50% position | |||
| 473 | + | alloc := webkit.WidgetGetAllocation(paned) | |||
| 474 | + | var splitPos int | |||
| 475 | + | if orientation == webkit.OrientationHorizontal { | |||
| 476 | + | splitPos = alloc.Width / 2 | |||
| 477 | + | } else { | |||
| 478 | + | splitPos = alloc.Height / 2 | |||
| 479 | + | } | |||
| 480 | + | if splitPos > 0 { | |||
| 481 | + | webkit.PanedSetPosition(paned, splitPos) | |||
| 482 | + | log.Printf("[workspace] Set initial paned position to 50%%: %d (orientation=%d, size=%dx%d)", splitPos, orientation, alloc.Width, alloc.Height) | |||
| 483 | + | } | |||
| 484 | + | return false | |||
| 485 | + | }, split) | |||
| 486 | + | ||||
| 487 | + | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | |||
| 488 | + | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | |||
| 489 | + | if target != nil && target.container != 0 { | |||
| 490 | + | wm.scheduleIdleGuarded(func() bool { | |||
| 491 | + | if target == nil || !target.widgetValid || target.container == 0 { | |||
| 492 | + | return false | |||
| 493 | + | } | |||
| 494 | + | // Show widget to reconnect WebKit rendering pipeline | |||
| 495 | + | webkit.WidgetShow(target.container) | |||
| 496 | + | // Force GTK to recalculate size and recreate rendering surface | |||
| 497 | + | webkit.WidgetQueueResize(target.container) | |||
| 498 | + | webkit.WidgetQueueDraw(target.container) | |||
| 499 | + | log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting") | |||
| 500 | + | return false | |||
| 501 | + | }, target) | |||
| 502 | + | } | |||
| 503 | + | ||||
| 430 | wm.viewToNode[newPane.webView] = newLeaf | 504 | wm.viewToNode[newPane.webView] = newLeaf | ||
| 431 | wm.ensureHover(newLeaf) | 505 | wm.ensureHover(newLeaf) | ||
| 432 | wm.ensureHover(target) | 506 | wm.ensureHover(target) | ||
| @@ -684,6 +758,45 @@ func (wm *WorkspaceManager) splitNode(target *paneNode, direction string) (*pane | |||||
| 684 | 758 | ||||
| 685 | webkit.WidgetShow(paned) | 759 | webkit.WidgetShow(paned) | ||
| 686 | 760 | ||||
| 761 | + | // CRITICAL: Set initial 50/50 split position after showing paned | |||
| 762 | + | // GTK needs the widget to be realized before we can set position based on allocation | |||
| 763 | + | // Schedule this to run after GTK has allocated space to the paned | |||
| 764 | + | wm.scheduleIdleGuarded(func() bool { | |||
| 765 | + | if !webkit.WidgetIsValid(paned) { | |||
| 766 | + | return false | |||
| 767 | + | } | |||
| 768 | + | // Get paned allocation to calculate 50% position | |||
| 769 | + | alloc := webkit.WidgetGetAllocation(paned) | |||
| 770 | + | var splitPos int | |||
| 771 | + | if orientation == webkit.OrientationHorizontal { | |||
| 772 | + | splitPos = alloc.Width / 2 | |||
| 773 | + | } else { | |||
| 774 | + | splitPos = alloc.Height / 2 | |||
| 775 | + | } | |||
| 776 | + | if splitPos > 0 { | |||
| 777 | + | webkit.PanedSetPosition(paned, splitPos) | |||
| 778 | + | log.Printf("[workspace] Set initial paned position to 50%%: %d (orientation=%d, size=%dx%d)", splitPos, orientation, alloc.Width, alloc.Height) | |||
| 779 | + | } | |||
| 780 | + | return false | |||
| 781 | + | }, split) | |||
| 782 | + | ||||
| 783 | + | // CRITICAL: Show the existing container and force GTK to recreate rendering surface after reparenting | |||
| 784 | + | // This reconnects WebKitGTK's rendering pipeline and fixes compositor sync issues | |||
| 785 | + | if target != nil && target.container != 0 { | |||
| 786 | + | wm.scheduleIdleGuarded(func() bool { | |||
| 787 | + | if target == nil || !target.widgetValid || target.container == 0 { | |||
| 788 | + | return false | |||
| 789 | + | } | |||
| 790 | + | // Show widget to reconnect WebKit rendering pipeline | |||
| 791 | + | webkit.WidgetShow(target.container) | |||
| 792 | + | // Force GTK to recalculate size and recreate rendering surface | |||
| 793 | + | webkit.WidgetQueueResize(target.container) | |||
| 794 | + | webkit.WidgetQueueDraw(target.container) | |||
| 795 | + | log.Printf("[workspace] Shown and queued resize+draw for target container after reparenting") | |||
| 796 | + | return false | |||
| 797 | + | }, target) | |||
| 798 | + | } | |||
| 799 | + | ||||
| 687 | wm.viewToNode[newPane.webView] = newLeaf | 800 | wm.viewToNode[newPane.webView] = newLeaf | ||
| 688 | wm.ensureHover(newLeaf) | 801 | wm.ensureHover(newLeaf) | ||
| 689 | 802 | ||||
M internal/app/browser/workspace_popup.go +203 -2
| @@ -134,21 +134,55 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||
| 134 | 134 | return nil | |
| 135 | 135 | } | |
| 136 | 136 | ||
| 137 | + | // Get the new node and popup WebView ID for tracking | |
| 138 | + | newNode := wm.viewToNode[newView] | |
| 139 | + | popupWebViewID := newView.ID() | |
| 140 | + | ||
| 137 | 141 | // Apply different behavior based on target type | |
| 138 | 142 | if isBlankTarget { | |
| 139 | 143 | log.Printf("[workspace] Treating _blank target as regular pane - no auto-close behavior") | |
| 140 | - | // For _blank targets, just ensure GUI - no popup-specific behavior | |
| 144 | + | // For _blank targets, set up parent relationship but no auto-close | |
| 145 | + | if newNode != nil { | |
| 146 | + | newNode.parentPane = node | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // Register close handler to clean up activePopupChildren tracking | |
| 150 | + | newView.RegisterCloseHandler(func() { | |
| 151 | + | log.Printf("[workspace] _blank popup requesting close via window.close()") | |
| 152 | + | // Remove this popup from parent's activePopupChildren | |
| 153 | + | if node != nil && popupWebViewID != "" { | |
| 154 | + | for i, childID := range node.activePopupChildren { | |
| 155 | + | if childID == popupWebViewID { | |
| 156 | + | node.activePopupChildren = append(node.activePopupChildren[:i], node.activePopupChildren[i+1:]...) | |
| 157 | + | log.Printf("[workspace] Removed _blank popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(node.activePopupChildren)) | |
| 158 | + | break | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | }) | |
| 141 | 163 | } else { | |
| 142 | 164 | log.Printf("[workspace] Treating as popup pane - applying popup-specific behavior") | |
| 143 | 165 | // Mark as popup for auto-close handling (OAuth flows, etc.) | |
| 144 | - | newNode := wm.viewToNode[newView] | |
| 145 | 166 | if newNode != nil { | |
| 146 | 167 | newNode.isPopup = true | |
| 168 | + | newNode.parentPane = node | |
| 147 | 169 | log.Printf("[workspace] Marked pane as popup for auto-close handling") | |
| 148 | 170 | ||
| 149 | 171 | // Register close handler for popup auto-close on window.close() | |
| 150 | 172 | newView.RegisterCloseHandler(func() { | |
| 151 | 173 | log.Printf("[workspace] Popup requesting close via window.close()") | |
| 174 | + | ||
| 175 | + | // Remove this popup from parent's activePopupChildren | |
| 176 | + | if node != nil && popupWebViewID != "" { | |
| 177 | + | for i, childID := range node.activePopupChildren { | |
| 178 | + | if childID == popupWebViewID { | |
| 179 | + | node.activePopupChildren = append(node.activePopupChildren[:i], node.activePopupChildren[i+1:]...) | |
| 180 | + | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(node.activePopupChildren)) | |
| 181 | + | break | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | } | |
| 185 | + | ||
| 152 | 186 | // Look up the node at close time | |
| 153 | 187 | if node := wm.viewToNode[newView]; node != nil && node.isPopup { | |
| 154 | 188 | log.Printf("[workspace] Closing popup pane") | |
| @@ -173,6 +207,37 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||
| 173 | 207 | } | |
| 174 | 208 | } | |
| 175 | 209 | ||
| 210 | + | // Track this popup in the parent's activePopupChildren (for both _blank and popup types) | |
| 211 | + | // This is critical to prevent OAuth flows from hijacking the parent pane via window.opener | |
| 212 | + | if node != nil && popupWebViewID != "" { | |
| 213 | + | if node.activePopupChildren == nil { | |
| 214 | + | node.activePopupChildren = make([]string, 0) | |
| 215 | + | } | |
| 216 | + | node.activePopupChildren = append(node.activePopupChildren, popupWebViewID) | |
| 217 | + | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(node.activePopupChildren)) | |
| 218 | + | ||
| 219 | + | // Register navigation policy handler for parent to block script-initiated navigations | |
| 220 | + | // when it has active popup children (prevents OAuth from hijacking parent pane) | |
| 221 | + | if node.pane != nil && node.pane.WebView() != nil { | |
| 222 | + | node.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |
| 223 | + | // Always allow user-initiated navigations | |
| 224 | + | if isUserGesture { | |
| 225 | + | return true | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | // Block script-initiated navigations if this pane has active popup children | |
| 229 | + | if len(node.activePopupChildren) > 0 { | |
| 230 | + | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(node.activePopupChildren), url) | |
| 231 | + | return false | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | // Allow if no active popups | |
| 235 | + | return true | |
| 236 | + | }) | |
| 237 | + | log.Printf("[workspace] Registered navigation policy handler for parent pane") | |
| 238 | + | } | |
| 239 | + | } | |
| 240 | + | ||
| 176 | 241 | // Ensure GUI components are available in the new pane | |
| 177 | 242 | wm.ensureGUIInPane(newPane) | |
| 178 | 243 | ||
| @@ -391,13 +456,56 @@ func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string | |||
| 391 | 456 | node.isPopup = true | |
| 392 | 457 | node.autoClose = wm.shouldAutoClose(url) | |
| 393 | 458 | ||
| 459 | + | // Track this popup in the parent's activePopupChildren | |
| 460 | + | if sourceNode != nil { | |
| 461 | + | if sourceNode.activePopupChildren == nil { | |
| 462 | + | sourceNode.activePopupChildren = make([]string, 0) | |
| 463 | + | } | |
| 464 | + | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren, popupWebViewID) | |
| 465 | + | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |
| 466 | + | ||
| 467 | + | // Register navigation policy handler for parent to block script-initiated navigations | |
| 468 | + | // when it has active popup children (prevents OAuth from hijacking parent pane) | |
| 469 | + | if sourceNode.pane != nil && sourceNode.pane.WebView() != nil { | |
| 470 | + | sourceNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |
| 471 | + | // Always allow user-initiated navigations | |
| 472 | + | if isUserGesture { | |
| 473 | + | return true | |
| 474 | + | } | |
| 475 | + | ||
| 476 | + | // Block script-initiated navigations if this pane has active popup children | |
| 477 | + | if len(sourceNode.activePopupChildren) > 0 { | |
| 478 | + | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url) | |
| 479 | + | return false | |
| 480 | + | } | |
| 481 | + | ||
| 482 | + | // Allow if no active popups | |
| 483 | + | return true | |
| 484 | + | }) | |
| 485 | + | log.Printf("[workspace] Registered navigation policy handler for parent pane") | |
| 486 | + | } | |
| 487 | + | } | |
| 488 | + | ||
| 394 | 489 | // Store requestID for deduplication cleanup | |
| 395 | 490 | if intent != nil { | |
| 396 | 491 | requestID = intent.RequestID | |
| 492 | + | node.requestID = requestID | |
| 397 | 493 | } | |
| 398 | 494 | ||
| 399 | 495 | // Apply window features from JavaScript intent | |
| 400 | 496 | wm.applyWindowFeatures(newView, intent, true) | |
| 497 | + | ||
| 498 | + | // Apply minimum size constraints to prevent compression | |
| 499 | + | var width, height int | |
| 500 | + | if intent != nil { | |
| 501 | + | if intent.Width != nil { | |
| 502 | + | width = *intent.Width | |
| 503 | + | } | |
| 504 | + | if intent.Height != nil { | |
| 505 | + | height = *intent.Height | |
| 506 | + | } | |
| 507 | + | } | |
| 508 | + | wm.applyPopupSizeConstraints(newView, width, height) | |
| 401 | 509 | } | |
| 402 | 510 | ||
| 403 | 511 | // Register close handler for popup auto-close | |
| @@ -409,6 +517,17 @@ func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string | |||
| 409 | 517 | wm.paneDeduplicator.ClearRequestID(requestID) | |
| 410 | 518 | } | |
| 411 | 519 | ||
| 520 | + | // Remove this popup from parent's activePopupChildren | |
| 521 | + | if sourceNode != nil && popupWebViewID != "" { | |
| 522 | + | for i, childID := range sourceNode.activePopupChildren { | |
| 523 | + | if childID == popupWebViewID { | |
| 524 | + | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren[:i], sourceNode.activePopupChildren[i+1:]...) | |
| 525 | + | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |
| 526 | + | break | |
| 527 | + | } | |
| 528 | + | } | |
| 529 | + | } | |
| 530 | + | ||
| 412 | 531 | if n := wm.viewToNode[newView]; n != nil && n.isPopup { | |
| 413 | 532 | time.AfterFunc(200*time.Millisecond, func() { | |
| 414 | 533 | wm.scheduleIdleGuarded(func() bool { | |
| @@ -486,6 +605,8 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView | |||
| 486 | 605 | return | |
| 487 | 606 | } | |
| 488 | 607 | node := wm.viewToNode[related] | |
| 608 | + | popupWebViewID := related.ID() | |
| 609 | + | ||
| 489 | 610 | if node != nil { | |
| 490 | 611 | node.windowType = webkit.WindowTypePopup | |
| 491 | 612 | node.windowFeatures = feat | |
| @@ -494,10 +615,62 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView | |||
| 494 | 615 | node.isPopup = true | |
| 495 | 616 | // Heuristic + config for auto-close intent | |
| 496 | 617 | node.autoClose = wm.shouldAutoClose(url) | |
| 618 | + | ||
| 619 | + | // Apply minimum size constraints to prevent compression | |
| 620 | + | var width, height int | |
| 621 | + | if feat != nil { | |
| 622 | + | width = feat.Width | |
| 623 | + | height = feat.Height | |
| 624 | + | } | |
| 625 | + | wm.applyPopupSizeConstraints(related, width, height) | |
| 497 | 626 | } | |
| 627 | + | ||
| 628 | + | // Track this popup in the parent's activePopupChildren | |
| 629 | + | // This is critical to prevent OAuth flows from hijacking the parent pane via window.opener | |
| 630 | + | if sourceNode != nil && popupWebViewID != "" { | |
| 631 | + | if sourceNode.activePopupChildren == nil { | |
| 632 | + | sourceNode.activePopupChildren = make([]string, 0) | |
| 633 | + | } | |
| 634 | + | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren, popupWebViewID) | |
| 635 | + | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |
| 636 | + | ||
| 637 | + | // Register navigation policy handler for parent to block script-initiated navigations | |
| 638 | + | // when it has active popup children (prevents OAuth from hijacking parent pane) | |
| 639 | + | if sourceNode.pane != nil && sourceNode.pane.WebView() != nil { | |
| 640 | + | sourceNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |
| 641 | + | // Always allow user-initiated navigations | |
| 642 | + | if isUserGesture { | |
| 643 | + | return true | |
| 644 | + | } | |
| 645 | + | ||
| 646 | + | // Block script-initiated navigations if this pane has active popup children | |
| 647 | + | if len(sourceNode.activePopupChildren) > 0 { | |
| 648 | + | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url) | |
| 649 | + | return false | |
| 650 | + | } | |
| 651 | + | ||
| 652 | + | // Allow if no active popups | |
| 653 | + | return true | |
| 654 | + | }) | |
| 655 | + | log.Printf("[workspace] Registered navigation policy handler for parent pane") | |
| 656 | + | } | |
| 657 | + | } | |
| 658 | + | ||
| 498 | 659 | // Pipe into existing auto-close flow only for popups (confirmed by detection) | |
| 499 | 660 | related.RegisterCloseHandler(func() { | |
| 500 | 661 | log.Printf("[workspace] Popup requesting close via window.close()") | |
| 662 | + | ||
| 663 | + | // Remove this popup from parent's activePopupChildren | |
| 664 | + | if sourceNode != nil && popupWebViewID != "" { | |
| 665 | + | for i, childID := range sourceNode.activePopupChildren { | |
| 666 | + | if childID == popupWebViewID { | |
| 667 | + | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren[:i], sourceNode.activePopupChildren[i+1:]...) | |
| 668 | + | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |
| 669 | + | break | |
| 670 | + | } | |
| 671 | + | } | |
| 672 | + | } | |
| 673 | + | ||
| 501 | 674 | if n := wm.viewToNode[related]; n != nil && n.isPopup { | |
| 502 | 675 | time.AfterFunc(200*time.Millisecond, func() { | |
| 503 | 676 | wm.scheduleIdleGuarded(func() bool { | |
| @@ -522,6 +695,34 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView | |||
| 522 | 695 | } | |
| 523 | 696 | } | |
| 524 | 697 | ||
| 698 | + | // applyPopupSizeConstraints applies minimum size constraints to prevent OAuth popup compression | |
| 699 | + | func (wm *WorkspaceManager) applyPopupSizeConstraints(view *webkit.WebView, width, height int) { | |
| 700 | + | minWidth := 500 // Default minimum width | |
| 701 | + | minHeight := 600 // Default minimum height | |
| 702 | + | ||
| 703 | + | // Use provided dimensions if valid | |
| 704 | + | if width > 0 { | |
| 705 | + | minWidth = width | |
| 706 | + | } | |
| 707 | + | if height > 0 { | |
| 708 | + | minHeight = height | |
| 709 | + | } | |
| 710 | + | ||
| 711 | + | // Apply to container | |
| 712 | + | if container := view.RootWidget(); container != 0 && webkit.WidgetIsValid(container) { | |
| 713 | + | webkit.WidgetSetSizeRequest(container, minWidth, minHeight) | |
| 714 | + | webkit.WidgetQueueResize(container) | |
| 715 | + | log.Printf("[workspace] Applied minimum size %dx%d to popup container=%#x", minWidth, minHeight, container) | |
| 716 | + | } | |
| 717 | + | ||
| 718 | + | // Apply to WebView widget | |
| 719 | + | if webViewWidget := view.Widget(); webViewWidget != 0 && webkit.WidgetIsValid(webViewWidget) { | |
| 720 | + | webkit.WidgetSetSizeRequest(webViewWidget, minWidth, minHeight) | |
| 721 | + | webkit.WidgetQueueResize(webViewWidget) | |
| 722 | + | log.Printf("[workspace] Applied minimum size %dx%d to popup webview=%#x", minWidth, minHeight, webViewWidget) | |
| 723 | + | } | |
| 724 | + | } | |
| 725 | + | ||
| 525 | 726 | // shouldAutoClose checks simple OAuth-like URL patterns and config flag | |
| 526 | 727 | func (wm *WorkspaceManager) shouldAutoClose(url string) bool { | |
| 527 | 728 | log.Printf("[workspace] shouldAutoClose called for URL: %s", url) | |
| @@ -134,21 +134,55 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||||
| 134 | return nil | 134 | return nil | ||
| 135 | } | 135 | } | ||
| 136 | 136 | ||||
| 137 | + | // Get the new node and popup WebView ID for tracking | |||
| 138 | + | newNode := wm.viewToNode[newView] | |||
| 139 | + | popupWebViewID := newView.ID() | |||
| 140 | + | ||||
| 137 | // Apply different behavior based on target type | 141 | // Apply different behavior based on target type | ||
| 138 | if isBlankTarget { | 142 | if isBlankTarget { | ||
| 139 | log.Printf("[workspace] Treating _blank target as regular pane - no auto-close behavior") | 143 | log.Printf("[workspace] Treating _blank target as regular pane - no auto-close behavior") | ||
| 140 | - | // For _blank targets, just ensure GUI - no popup-specific behavior | 144 | + | // For _blank targets, set up parent relationship but no auto-close |
| 145 | + | if newNode != nil { | |||
| 146 | + | newNode.parentPane = node | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | // Register close handler to clean up activePopupChildren tracking | |||
| 150 | + | newView.RegisterCloseHandler(func() { | |||
| 151 | + | log.Printf("[workspace] _blank popup requesting close via window.close()") | |||
| 152 | + | // Remove this popup from parent's activePopupChildren | |||
| 153 | + | if node != nil && popupWebViewID != "" { | |||
| 154 | + | for i, childID := range node.activePopupChildren { | |||
| 155 | + | if childID == popupWebViewID { | |||
| 156 | + | node.activePopupChildren = append(node.activePopupChildren[:i], node.activePopupChildren[i+1:]...) | |||
| 157 | + | log.Printf("[workspace] Removed _blank popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(node.activePopupChildren)) | |||
| 158 | + | break | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | } | |||
| 162 | + | }) | |||
| 141 | } else { | 163 | } else { | ||
| 142 | log.Printf("[workspace] Treating as popup pane - applying popup-specific behavior") | 164 | log.Printf("[workspace] Treating as popup pane - applying popup-specific behavior") | ||
| 143 | // Mark as popup for auto-close handling (OAuth flows, etc.) | 165 | // Mark as popup for auto-close handling (OAuth flows, etc.) | ||
| 144 | - | newNode := wm.viewToNode[newView] | |||
| 145 | if newNode != nil { | 166 | if newNode != nil { | ||
| 146 | newNode.isPopup = true | 167 | newNode.isPopup = true | ||
| 168 | + | newNode.parentPane = node | |||
| 147 | log.Printf("[workspace] Marked pane as popup for auto-close handling") | 169 | log.Printf("[workspace] Marked pane as popup for auto-close handling") | ||
| 148 | 170 | ||||
| 149 | // Register close handler for popup auto-close on window.close() | 171 | // Register close handler for popup auto-close on window.close() | ||
| 150 | newView.RegisterCloseHandler(func() { | 172 | newView.RegisterCloseHandler(func() { | ||
| 151 | log.Printf("[workspace] Popup requesting close via window.close()") | 173 | log.Printf("[workspace] Popup requesting close via window.close()") | ||
| 174 | + | ||||
| 175 | + | // Remove this popup from parent's activePopupChildren | |||
| 176 | + | if node != nil && popupWebViewID != "" { | |||
| 177 | + | for i, childID := range node.activePopupChildren { | |||
| 178 | + | if childID == popupWebViewID { | |||
| 179 | + | node.activePopupChildren = append(node.activePopupChildren[:i], node.activePopupChildren[i+1:]...) | |||
| 180 | + | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(node.activePopupChildren)) | |||
| 181 | + | break | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | } | |||
| 185 | + | ||||
| 152 | // Look up the node at close time | 186 | // Look up the node at close time | ||
| 153 | if node := wm.viewToNode[newView]; node != nil && node.isPopup { | 187 | if node := wm.viewToNode[newView]; node != nil && node.isPopup { | ||
| 154 | log.Printf("[workspace] Closing popup pane") | 188 | log.Printf("[workspace] Closing popup pane") | ||
| @@ -173,6 +207,37 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||||
| 173 | } | 207 | } | ||
| 174 | } | 208 | } | ||
| 175 | 209 | ||||
| 210 | + | // Track this popup in the parent's activePopupChildren (for both _blank and popup types) | |||
| 211 | + | // This is critical to prevent OAuth flows from hijacking the parent pane via window.opener | |||
| 212 | + | if node != nil && popupWebViewID != "" { | |||
| 213 | + | if node.activePopupChildren == nil { | |||
| 214 | + | node.activePopupChildren = make([]string, 0) | |||
| 215 | + | } | |||
| 216 | + | node.activePopupChildren = append(node.activePopupChildren, popupWebViewID) | |||
| 217 | + | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(node.activePopupChildren)) | |||
| 218 | + | ||||
| 219 | + | // Register navigation policy handler for parent to block script-initiated navigations | |||
| 220 | + | // when it has active popup children (prevents OAuth from hijacking parent pane) | |||
| 221 | + | if node.pane != nil && node.pane.WebView() != nil { | |||
| 222 | + | node.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |||
| 223 | + | // Always allow user-initiated navigations | |||
| 224 | + | if isUserGesture { | |||
| 225 | + | return true | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | // Block script-initiated navigations if this pane has active popup children | |||
| 229 | + | if len(node.activePopupChildren) > 0 { | |||
| 230 | + | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(node.activePopupChildren), url) | |||
| 231 | + | return false | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | // Allow if no active popups | |||
| 235 | + | return true | |||
| 236 | + | }) | |||
| 237 | + | log.Printf("[workspace] Registered navigation policy handler for parent pane") | |||
| 238 | + | } | |||
| 239 | + | } | |||
| 240 | + | ||||
| 176 | // Ensure GUI components are available in the new pane | 241 | // Ensure GUI components are available in the new pane | ||
| 177 | wm.ensureGUIInPane(newPane) | 242 | wm.ensureGUIInPane(newPane) | ||
| 178 | 243 | ||||
| @@ -391,13 +456,56 @@ func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string | |||||
| 391 | node.isPopup = true | 456 | node.isPopup = true | ||
| 392 | node.autoClose = wm.shouldAutoClose(url) | 457 | node.autoClose = wm.shouldAutoClose(url) | ||
| 393 | 458 | ||||
| 459 | + | // Track this popup in the parent's activePopupChildren | |||
| 460 | + | if sourceNode != nil { | |||
| 461 | + | if sourceNode.activePopupChildren == nil { | |||
| 462 | + | sourceNode.activePopupChildren = make([]string, 0) | |||
| 463 | + | } | |||
| 464 | + | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren, popupWebViewID) | |||
| 465 | + | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |||
| 466 | + | ||||
| 467 | + | // Register navigation policy handler for parent to block script-initiated navigations | |||
| 468 | + | // when it has active popup children (prevents OAuth from hijacking parent pane) | |||
| 469 | + | if sourceNode.pane != nil && sourceNode.pane.WebView() != nil { | |||
| 470 | + | sourceNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |||
| 471 | + | // Always allow user-initiated navigations | |||
| 472 | + | if isUserGesture { | |||
| 473 | + | return true | |||
| 474 | + | } | |||
| 475 | + | ||||
| 476 | + | // Block script-initiated navigations if this pane has active popup children | |||
| 477 | + | if len(sourceNode.activePopupChildren) > 0 { | |||
| 478 | + | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url) | |||
| 479 | + | return false | |||
| 480 | + | } | |||
| 481 | + | ||||
| 482 | + | // Allow if no active popups | |||
| 483 | + | return true | |||
| 484 | + | }) | |||
| 485 | + | log.Printf("[workspace] Registered navigation policy handler for parent pane") | |||
| 486 | + | } | |||
| 487 | + | } | |||
| 488 | + | ||||
| 394 | // Store requestID for deduplication cleanup | 489 | // Store requestID for deduplication cleanup | ||
| 395 | if intent != nil { | 490 | if intent != nil { | ||
| 396 | requestID = intent.RequestID | 491 | requestID = intent.RequestID | ||
| 492 | + | node.requestID = requestID | |||
| 397 | } | 493 | } | ||
| 398 | 494 | ||||
| 399 | // Apply window features from JavaScript intent | 495 | // Apply window features from JavaScript intent | ||
| 400 | wm.applyWindowFeatures(newView, intent, true) | 496 | wm.applyWindowFeatures(newView, intent, true) | ||
| 497 | + | ||||
| 498 | + | // Apply minimum size constraints to prevent compression | |||
| 499 | + | var width, height int | |||
| 500 | + | if intent != nil { | |||
| 501 | + | if intent.Width != nil { | |||
| 502 | + | width = *intent.Width | |||
| 503 | + | } | |||
| 504 | + | if intent.Height != nil { | |||
| 505 | + | height = *intent.Height | |||
| 506 | + | } | |||
| 507 | + | } | |||
| 508 | + | wm.applyPopupSizeConstraints(newView, width, height) | |||
| 401 | } | 509 | } | ||
| 402 | 510 | ||||
| 403 | // Register close handler for popup auto-close | 511 | // Register close handler for popup auto-close | ||
| @@ -409,6 +517,17 @@ func (wm *WorkspaceManager) handleIntentAsPopup(sourceNode *paneNode, url string | |||||
| 409 | wm.paneDeduplicator.ClearRequestID(requestID) | 517 | wm.paneDeduplicator.ClearRequestID(requestID) | ||
| 410 | } | 518 | } | ||
| 411 | 519 | ||||
| 520 | + | // Remove this popup from parent's activePopupChildren | |||
| 521 | + | if sourceNode != nil && popupWebViewID != "" { | |||
| 522 | + | for i, childID := range sourceNode.activePopupChildren { | |||
| 523 | + | if childID == popupWebViewID { | |||
| 524 | + | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren[:i], sourceNode.activePopupChildren[i+1:]...) | |||
| 525 | + | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |||
| 526 | + | break | |||
| 527 | + | } | |||
| 528 | + | } | |||
| 529 | + | } | |||
| 530 | + | ||||
| 412 | if n := wm.viewToNode[newView]; n != nil && n.isPopup { | 531 | if n := wm.viewToNode[newView]; n != nil && n.isPopup { | ||
| 413 | time.AfterFunc(200*time.Millisecond, func() { | 532 | time.AfterFunc(200*time.Millisecond, func() { | ||
| 414 | wm.scheduleIdleGuarded(func() bool { | 533 | wm.scheduleIdleGuarded(func() bool { | ||
| @@ -486,6 +605,8 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView | |||||
| 486 | return | 605 | return | ||
| 487 | } | 606 | } | ||
| 488 | node := wm.viewToNode[related] | 607 | node := wm.viewToNode[related] | ||
| 608 | + | popupWebViewID := related.ID() | |||
| 609 | + | ||||
| 489 | if node != nil { | 610 | if node != nil { | ||
| 490 | node.windowType = webkit.WindowTypePopup | 611 | node.windowType = webkit.WindowTypePopup | ||
| 491 | node.windowFeatures = feat | 612 | node.windowFeatures = feat | ||
| @@ -494,10 +615,62 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView | |||||
| 494 | node.isPopup = true | 615 | node.isPopup = true | ||
| 495 | // Heuristic + config for auto-close intent | 616 | // Heuristic + config for auto-close intent | ||
| 496 | node.autoClose = wm.shouldAutoClose(url) | 617 | node.autoClose = wm.shouldAutoClose(url) | ||
| 618 | + | ||||
| 619 | + | // Apply minimum size constraints to prevent compression | |||
| 620 | + | var width, height int | |||
| 621 | + | if feat != nil { | |||
| 622 | + | width = feat.Width | |||
| 623 | + | height = feat.Height | |||
| 624 | + | } | |||
| 625 | + | wm.applyPopupSizeConstraints(related, width, height) | |||
| 497 | } | 626 | } | ||
| 627 | + | ||||
| 628 | + | // Track this popup in the parent's activePopupChildren | |||
| 629 | + | // This is critical to prevent OAuth flows from hijacking the parent pane via window.opener | |||
| 630 | + | if sourceNode != nil && popupWebViewID != "" { | |||
| 631 | + | if sourceNode.activePopupChildren == nil { | |||
| 632 | + | sourceNode.activePopupChildren = make([]string, 0) | |||
| 633 | + | } | |||
| 634 | + | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren, popupWebViewID) | |||
| 635 | + | log.Printf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |||
| 636 | + | ||||
| 637 | + | // Register navigation policy handler for parent to block script-initiated navigations | |||
| 638 | + | // when it has active popup children (prevents OAuth from hijacking parent pane) | |||
| 639 | + | if sourceNode.pane != nil && sourceNode.pane.WebView() != nil { | |||
| 640 | + | sourceNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool { | |||
| 641 | + | // Always allow user-initiated navigations | |||
| 642 | + | if isUserGesture { | |||
| 643 | + | return true | |||
| 644 | + | } | |||
| 645 | + | ||||
| 646 | + | // Block script-initiated navigations if this pane has active popup children | |||
| 647 | + | if len(sourceNode.activePopupChildren) > 0 { | |||
| 648 | + | log.Printf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url) | |||
| 649 | + | return false | |||
| 650 | + | } | |||
| 651 | + | ||||
| 652 | + | // Allow if no active popups | |||
| 653 | + | return true | |||
| 654 | + | }) | |||
| 655 | + | log.Printf("[workspace] Registered navigation policy handler for parent pane") | |||
| 656 | + | } | |||
| 657 | + | } | |||
| 658 | + | ||||
| 498 | // Pipe into existing auto-close flow only for popups (confirmed by detection) | 659 | // Pipe into existing auto-close flow only for popups (confirmed by detection) | ||
| 499 | related.RegisterCloseHandler(func() { | 660 | related.RegisterCloseHandler(func() { | ||
| 500 | log.Printf("[workspace] Popup requesting close via window.close()") | 661 | log.Printf("[workspace] Popup requesting close via window.close()") | ||
| 662 | + | ||||
| 663 | + | // Remove this popup from parent's activePopupChildren | |||
| 664 | + | if sourceNode != nil && popupWebViewID != "" { | |||
| 665 | + | for i, childID := range sourceNode.activePopupChildren { | |||
| 666 | + | if childID == popupWebViewID { | |||
| 667 | + | sourceNode.activePopupChildren = append(sourceNode.activePopupChildren[:i], sourceNode.activePopupChildren[i+1:]...) | |||
| 668 | + | log.Printf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren)) | |||
| 669 | + | break | |||
| 670 | + | } | |||
| 671 | + | } | |||
| 672 | + | } | |||
| 673 | + | ||||
| 501 | if n := wm.viewToNode[related]; n != nil && n.isPopup { | 674 | if n := wm.viewToNode[related]; n != nil && n.isPopup { | ||
| 502 | time.AfterFunc(200*time.Millisecond, func() { | 675 | time.AfterFunc(200*time.Millisecond, func() { | ||
| 503 | wm.scheduleIdleGuarded(func() bool { | 676 | wm.scheduleIdleGuarded(func() bool { | ||
| @@ -522,6 +695,34 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView | |||||
| 522 | } | 695 | } | ||
| 523 | } | 696 | } | ||
| 524 | 697 | ||||
| 698 | + | // applyPopupSizeConstraints applies minimum size constraints to prevent OAuth popup compression | |||
| 699 | + | func (wm *WorkspaceManager) applyPopupSizeConstraints(view *webkit.WebView, width, height int) { | |||
| 700 | + | minWidth := 500 // Default minimum width | |||
| 701 | + | minHeight := 600 // Default minimum height | |||
| 702 | + | ||||
| 703 | + | // Use provided dimensions if valid | |||
| 704 | + | if width > 0 { | |||
| 705 | + | minWidth = width | |||
| 706 | + | } | |||
| 707 | + | if height > 0 { | |||
| 708 | + | minHeight = height | |||
| 709 | + | } | |||
| 710 | + | ||||
| 711 | + | // Apply to container | |||
| 712 | + | if container := view.RootWidget(); container != 0 && webkit.WidgetIsValid(container) { | |||
| 713 | + | webkit.WidgetSetSizeRequest(container, minWidth, minHeight) | |||
| 714 | + | webkit.WidgetQueueResize(container) | |||
| 715 | + | log.Printf("[workspace] Applied minimum size %dx%d to popup container=%#x", minWidth, minHeight, container) | |||
| 716 | + | } | |||
| 717 | + | ||||
| 718 | + | // Apply to WebView widget | |||
| 719 | + | if webViewWidget := view.Widget(); webViewWidget != 0 && webkit.WidgetIsValid(webViewWidget) { | |||
| 720 | + | webkit.WidgetSetSizeRequest(webViewWidget, minWidth, minHeight) | |||
| 721 | + | webkit.WidgetQueueResize(webViewWidget) | |||
| 722 | + | log.Printf("[workspace] Applied minimum size %dx%d to popup webview=%#x", minWidth, minHeight, webViewWidget) | |||
| 723 | + | } | |||
| 724 | + | } | |||
| 725 | + | ||||
| 525 | // shouldAutoClose checks simple OAuth-like URL patterns and config flag | 726 | // shouldAutoClose checks simple OAuth-like URL patterns and config flag | ||
| 526 | func (wm *WorkspaceManager) shouldAutoClose(url string) bool { | 727 | func (wm *WorkspaceManager) shouldAutoClose(url string) bool { | ||
| 527 | log.Printf("[workspace] shouldAutoClose called for URL: %s", url) | 728 | log.Printf("[workspace] shouldAutoClose called for URL: %s", url) | ||
M internal/app/browser/workspace_types.go +2 -0
| @@ -60,7 +60,9 @@ type paneNode struct { | |||
| 60 | 60 | windowFeatures *webkit.WindowFeatures // Features if popup | |
| 61 | 61 | isRelated bool // Shares context | |
| 62 | 62 | parentPane *paneNode // Parent for related views | |
| 63 | + | activePopupChildren []string // WebView IDs of active popup children (for tracking related popups) | |
| 63 | 64 | autoClose bool // Auto-close on OAuth success | |
| 65 | + | requestID string // Request ID for popup (used for localStorage cleanup) | |
| 64 | 66 | hoverToken uintptr | |
| 65 | 67 | focusControllerToken uintptr | |
| 66 | 68 | pendingHoverReattach bool | |
| @@ -60,7 +60,9 @@ type paneNode struct { | |||||
| 60 | windowFeatures *webkit.WindowFeatures // Features if popup | 60 | windowFeatures *webkit.WindowFeatures // Features if popup | ||
| 61 | isRelated bool // Shares context | 61 | isRelated bool // Shares context | ||
| 62 | parentPane *paneNode // Parent for related views | 62 | parentPane *paneNode // Parent for related views | ||
| 63 | + | activePopupChildren []string // WebView IDs of active popup children (for tracking related popups) | |||
| 63 | autoClose bool // Auto-close on OAuth success | 64 | autoClose bool // Auto-close on OAuth success | ||
| 65 | + | requestID string // Request ID for popup (used for localStorage cleanup) | |||
| 64 | hoverToken uintptr | 66 | hoverToken uintptr | ||
| 65 | focusControllerToken uintptr | 67 | focusControllerToken uintptr | ||
| 66 | pendingHoverReattach bool | 68 | pendingHoverReattach bool | ||
M internal/app/messaging/handler.go +1 -1
| @@ -355,7 +355,7 @@ func (h *Handler) handleQuery(msg Message) { | |||
| 355 | 355 | } else { | |
| 356 | 356 | // Prefer unified page-world API; fallback to legacy global function | |
| 357 | 357 | script := "(window.__dumber?.omnibox?.suggestions ? window.__dumber.omnibox.suggestions(" + string(b) + ") : (window.__dumber_omnibox_suggestions && window.__dumber_omnibox_suggestions(" + string(b) + ")))" | |
| 358 | - | log.Printf("[DEBUG] Injecting omnibox suggestions: %s", script) | |
| 358 | + | log.Print("[DEBUG] Injecting omnibox suggestions") | |
| 359 | 359 | if injErr := h.webView.InjectScript(script); injErr != nil { | |
| 360 | 360 | log.Printf("[ERROR] Failed to inject suggestions script: %v", injErr) | |
| 361 | 361 | } else { | |
| @@ -355,7 +355,7 @@ func (h *Handler) handleQuery(msg Message) { | |||||
| 355 | } else { | 355 | } else { | ||
| 356 | // Prefer unified page-world API; fallback to legacy global function | 356 | // Prefer unified page-world API; fallback to legacy global function | ||
| 357 | script := "(window.__dumber?.omnibox?.suggestions ? window.__dumber.omnibox.suggestions(" + string(b) + ") : (window.__dumber_omnibox_suggestions && window.__dumber_omnibox_suggestions(" + string(b) + ")))" | 357 | script := "(window.__dumber?.omnibox?.suggestions ? window.__dumber.omnibox.suggestions(" + string(b) + ") : (window.__dumber_omnibox_suggestions && window.__dumber_omnibox_suggestions(" + string(b) + ")))" | ||
| 358 | - | log.Printf("[DEBUG] Injecting omnibox suggestions: %s", script) | 358 | + | log.Print("[DEBUG] Injecting omnibox suggestions") |
| 359 | if injErr := h.webView.InjectScript(script); injErr != nil { | 359 | if injErr := h.webView.InjectScript(script); injErr != nil { | ||
| 360 | log.Printf("[ERROR] Failed to inject suggestions script: %v", injErr) | 360 | log.Printf("[ERROR] Failed to inject suggestions script: %v", injErr) | ||
| 361 | } else { | 361 | } else { | ||
M internal/services/browser.go +0 -2
| @@ -662,7 +662,5 @@ func (s *BrowserService) handleFaviconURIChanged(pageURL string, faviconURI stri | |||
| 662 | 662 | faviconNullString := sql.NullString{String: faviconURI, Valid: true} | |
| 663 | 663 | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | |
| 664 | 664 | log.Printf("[browser] Failed to update favicon URI in database for %s: %v", pageURL, err) | |
| 665 | - | } else { | |
| 666 | - | log.Printf("[browser] Successfully stored favicon URI for %s: %s", pageURL, faviconURI) | |
| 667 | 665 | } | |
| 668 | 666 | } | |
| @@ -662,7 +662,5 @@ func (s *BrowserService) handleFaviconURIChanged(pageURL string, faviconURI stri | |||||
| 662 | faviconNullString := sql.NullString{String: faviconURI, Valid: true} | 662 | faviconNullString := sql.NullString{String: faviconURI, Valid: true} | ||
| 663 | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | 663 | if err := s.dbQueries.UpdateHistoryFavicon(ctx, faviconNullString, pageURL); err != nil { | ||
| 664 | log.Printf("[browser] Failed to update favicon URI in database for %s: %v", pageURL, err) | 664 | log.Printf("[browser] Failed to update favicon URI in database for %s: %v", pageURL, err) | ||
| 665 | - | } else { | |||
| 666 | - | log.Printf("[browser] Successfully stored favicon URI for %s: %s", pageURL, faviconURI) | |||
| 667 | } | 665 | } | ||
| 668 | } | 666 | } | ||
M pkg/webkit/webview.go +12 -7
| @@ -32,13 +32,14 @@ type WebView struct { | |||
| 32 | 32 | uriHandler func(uri string) | |
| 33 | 33 | faviconHandler func(data []byte) | |
| 34 | 34 | faviconURIHandler func(pageURI, faviconURI string) | |
| 35 | - | zoomHandler func(level float64) | |
| 36 | - | popupHandler func(string) *WebView | |
| 37 | - | closeHandler func() | |
| 38 | - | useDomZoom bool | |
| 39 | - | domZoomSeed float64 | |
| 40 | - | container uintptr | |
| 41 | - | id uintptr // WebView unique identifier | |
| 35 | + | zoomHandler func(level float64) | |
| 36 | + | popupHandler func(string) *WebView | |
| 37 | + | closeHandler func() | |
| 38 | + | navigationPolicyHandler func(url string, isUserGesture bool) bool | |
| 39 | + | useDomZoom bool | |
| 40 | + | domZoomSeed float64 | |
| 41 | + | container uintptr | |
| 42 | + | id uintptr // WebView unique identifier | |
| 42 | 43 | ||
| 43 | 44 | // Window type fields (no-op in stub) | |
| 44 | 45 | windowType WindowType | |
| @@ -156,6 +157,10 @@ func (w *WebView) RegisterPopupHandler(cb func(string) *WebView) { w.popupHandle | |||
| 156 | 157 | ||
| 157 | 158 | func (w *WebView) RegisterCloseHandler(cb func()) { w.closeHandler = cb } | |
| 158 | 159 | ||
| 160 | + | func (w *WebView) RegisterNavigationPolicyHandler(cb func(url string, isUserGesture bool) bool) { | |
| 161 | + | w.navigationPolicyHandler = cb | |
| 162 | + | } | |
| 163 | + | ||
| 159 | 164 | func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Called from CGO WebKit callbacks | |
| 160 | 165 | if w != nil && w.msgHandler != nil { | |
| 161 | 166 | w.msgHandler(payload) | |
| @@ -32,13 +32,14 @@ type WebView struct { | |||||
| 32 | uriHandler func(uri string) | 32 | uriHandler func(uri string) | ||
| 33 | faviconHandler func(data []byte) | 33 | faviconHandler func(data []byte) | ||
| 34 | faviconURIHandler func(pageURI, faviconURI string) | 34 | faviconURIHandler func(pageURI, faviconURI string) | ||
| 35 | - | zoomHandler func(level float64) | 35 | + | zoomHandler func(level float64) |
| 36 | - | popupHandler func(string) *WebView | 36 | + | popupHandler func(string) *WebView |
| 37 | - | closeHandler func() | 37 | + | closeHandler func() |
| 38 | - | useDomZoom bool | 38 | + | navigationPolicyHandler func(url string, isUserGesture bool) bool |
| 39 | - | domZoomSeed float64 | 39 | + | useDomZoom bool |
| 40 | - | container uintptr | 40 | + | domZoomSeed float64 |
| 41 | - | id uintptr // WebView unique identifier | 41 | + | container uintptr |
| 42 | + | id uintptr // WebView unique identifier | |||
| 42 | 43 | ||||
| 43 | // Window type fields (no-op in stub) | 44 | // Window type fields (no-op in stub) | ||
| 44 | windowType WindowType | 45 | windowType WindowType | ||
| @@ -156,6 +157,10 @@ func (w *WebView) RegisterPopupHandler(cb func(string) *WebView) { w.popupHandle | |||||
| 156 | 157 | ||||
| 157 | func (w *WebView) RegisterCloseHandler(cb func()) { w.closeHandler = cb } | 158 | func (w *WebView) RegisterCloseHandler(cb func()) { w.closeHandler = cb } | ||
| 158 | 159 | ||||
| 160 | + | func (w *WebView) RegisterNavigationPolicyHandler(cb func(url string, isUserGesture bool) bool) { | |||
| 161 | + | w.navigationPolicyHandler = cb | |||
| 162 | + | } | |||
| 163 | + | ||||
| 159 | func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Called from CGO WebKit callbacks | 164 | func (w *WebView) dispatchScriptMessage(payload string) { //nolint:unused // Called from CGO WebKit callbacks | ||
| 160 | if w != nil && w.msgHandler != nil { | 165 | if w != nil && w.msgHandler != nil { | ||
| 161 | w.msgHandler(payload) | 166 | w.msgHandler(payload) | ||
M pkg/webkit/webview_cgo.go +76 -11
| @@ -80,7 +80,10 @@ static void on_webview_close(WebKitWebView* web_view, gpointer user_data) { | |||
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | 82 | ||
| 83 | - | // Handle decide-policy signal for new window requests - more robust than create signal | |
| 83 | + | // Forward declare navigation policy handler | |
| 84 | + | extern gboolean goHandleNavigationPolicy(unsigned long view_id, char* uri, gboolean is_user_gesture); | |
| 85 | + | ||
| 86 | + | // Handle decide-policy signal for new window requests and navigations | |
| 84 | 87 | static gboolean on_decide_policy(WebKitWebView *web_view, | |
| 85 | 88 | WebKitPolicyDecision *decision, | |
| 86 | 89 | WebKitPolicyDecisionType type, | |
| @@ -90,6 +93,32 @@ static gboolean on_decide_policy(WebKitWebView *web_view, | |||
| 90 | 93 | const char* type_name = (type >= 0 && type < 3) ? type_names[type] : "UNKNOWN"; | |
| 91 | 94 | printf("[webkit-policy] Policy decision: type=%s (%d)\n", type_name, type); | |
| 92 | 95 | ||
| 96 | + | unsigned long view_id = (unsigned long)user_data; | |
| 97 | + | ||
| 98 | + | if (type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { | |
| 99 | + | WebKitNavigationPolicyDecision *nav_decision = | |
| 100 | + | WEBKIT_NAVIGATION_POLICY_DECISION(decision); | |
| 101 | + | WebKitNavigationAction *action = | |
| 102 | + | webkit_navigation_policy_decision_get_navigation_action(nav_decision); | |
| 103 | + | WebKitURIRequest *request = | |
| 104 | + | webkit_navigation_action_get_request(action); | |
| 105 | + | const char* uri = webkit_uri_request_get_uri(request); | |
| 106 | + | gboolean is_user_gesture = webkit_navigation_action_is_user_gesture(action); | |
| 107 | + | ||
| 108 | + | // Ask Go if this navigation should be allowed | |
| 109 | + | gboolean allowed = goHandleNavigationPolicy(view_id, (char*)uri, is_user_gesture); | |
| 110 | + | ||
| 111 | + | if (!allowed) { | |
| 112 | + | printf("[webkit-policy] Navigation BLOCKED by Go handler: uri=%s is_user_gesture=%d\n", uri, is_user_gesture); | |
| 113 | + | webkit_policy_decision_ignore(decision); | |
| 114 | + | return TRUE; | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // Allow navigation | |
| 118 | + | webkit_policy_decision_use(decision); | |
| 119 | + | return TRUE; | |
| 120 | + | } | |
| 121 | + | ||
| 93 | 122 | if (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { | |
| 94 | 123 | WebKitNavigationPolicyDecision *nav_decision = | |
| 95 | 124 | WEBKIT_NAVIGATION_POLICY_DECISION(decision); | |
| @@ -111,7 +140,6 @@ static gboolean on_decide_policy(WebKitWebView *web_view, | |||
| 111 | 140 | guint mouse_button = webkit_navigation_action_get_mouse_button(action); | |
| 112 | 141 | ||
| 113 | 142 | // Call Go handler for popup creation | |
| 114 | - | unsigned long view_id = (unsigned long)user_data; | |
| 115 | 143 | gboolean handled = goHandleNewWindowPolicy(view_id, (char*)uri, nav_type, (char*)frame_name, | |
| 116 | 144 | is_user_gesture, modifiers, mouse_button); | |
| 117 | 145 | ||
| @@ -248,9 +276,9 @@ static GtkWidget* new_webview_related(WebKitWebView* related_view, WebKitUserCon | |||
| 248 | 276 | ||
| 249 | 277 | static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) { | |
| 250 | 278 | if (!web_view) return; | |
| 251 | - | // Create signal is now handled in window_handler_cgo.go | |
| 252 | - | // Use ConnectCreateSignal from Go to connect the signal | |
| 253 | - | printf("[webkit-debug] Policy handler connected (create signal handled separately)\n"); | |
| 279 | + | // Connect decide-policy signal for navigation control | |
| 280 | + | g_signal_connect_data(G_OBJECT(web_view), "decide-policy", G_CALLBACK(on_decide_policy), (gpointer)id, NULL, 0); | |
| 281 | + | printf("[webkit-debug] Policy handler connected for WebView id: %lu\n", id); | |
| 254 | 282 | } | |
| 255 | 283 | ||
| 256 | 284 | // Connect close signal to a WebView for popup auto-close functionality | |
| @@ -987,12 +1015,13 @@ type WebView struct { | |||
| 987 | 1015 | wasReparented bool // Track if this WebView has been reparented | |
| 988 | 1016 | ||
| 989 | 1017 | // Window type detection and relationships | |
| 990 | - | windowType WindowType | |
| 991 | - | windowFeatures *WindowFeatures | |
| 992 | - | windowTypeCallback func(WindowType, *WindowFeatures) | |
| 993 | - | parentWebView *WebView | |
| 994 | - | isRelated bool | |
| 995 | - | isActive bool // Track if this WebView is currently active/focused | |
| 1018 | + | windowType WindowType | |
| 1019 | + | windowFeatures *WindowFeatures | |
| 1020 | + | windowTypeCallback func(WindowType, *WindowFeatures) | |
| 1021 | + | parentWebView *WebView | |
| 1022 | + | isRelated bool | |
| 1023 | + | isActive bool // Track if this WebView is currently active/focused | |
| 1024 | + | navigationPolicyHandler func(url string, isUserGesture bool) bool // Handler to decide if navigation should be allowed | |
| 996 | 1025 | } | |
| 997 | 1026 | ||
| 998 | 1027 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | |
| @@ -1769,6 +1798,11 @@ func (w *WebView) RegisterCloseHandler(cb func()) { | |||
| 1769 | 1798 | } | |
| 1770 | 1799 | } | |
| 1771 | 1800 | ||
| 1801 | + | // RegisterNavigationPolicyHandler sets a handler to control navigation decisions | |
| 1802 | + | func (w *WebView) RegisterNavigationPolicyHandler(cb func(url string, isUserGesture bool) bool) { | |
| 1803 | + | w.navigationPolicyHandler = cb | |
| 1804 | + | } | |
| 1805 | + | ||
| 1772 | 1806 | // EnablePrintInterception connects the print signal to intercept Ctrl+P | |
| 1773 | 1807 | func (w *WebView) EnablePrintInterception() { | |
| 1774 | 1808 | if w.native != nil && w.native.wv != nil { | |
| @@ -2370,6 +2404,37 @@ func goHandleConsoleMessage(message *C.char) { | |||
| 2370 | 2404 | logging.CaptureWebKitLog(messageText) | |
| 2371 | 2405 | } | |
| 2372 | 2406 | ||
| 2407 | + | //export goHandleNavigationPolicy | |
| 2408 | + | func goHandleNavigationPolicy(viewID C.ulong, uri *C.char, isUserGesture C.gboolean) C.gboolean { | |
| 2409 | + | id := uintptr(viewID) | |
| 2410 | + | urlStr := C.GoString(uri) | |
| 2411 | + | userGesture := isUserGesture != 0 | |
| 2412 | + | ||
| 2413 | + | log.Printf("[navigation-policy] WebView %d navigating to: %s (user_gesture=%t)", id, urlStr, userGesture) | |
| 2414 | + | ||
| 2415 | + | // Find the WebView | |
| 2416 | + | regMu.RLock() | |
| 2417 | + | view, exists := viewByID[id] | |
| 2418 | + | regMu.RUnlock() | |
| 2419 | + | ||
| 2420 | + | if !exists || view == nil { | |
| 2421 | + | log.Printf("[navigation-policy] WebView %d not found, allowing navigation", id) | |
| 2422 | + | return C.TRUE // Allow by default if view not found | |
| 2423 | + | } | |
| 2424 | + | ||
| 2425 | + | // Check if this webview has a navigation policy handler | |
| 2426 | + | if view.navigationPolicyHandler != nil { | |
| 2427 | + | allowed := view.navigationPolicyHandler(urlStr, userGesture) | |
| 2428 | + | if !allowed { | |
| 2429 | + | log.Printf("[navigation-policy] WebView %d: Navigation BLOCKED by handler", id) | |
| 2430 | + | return C.FALSE | |
| 2431 | + | } | |
| 2432 | + | } | |
| 2433 | + | ||
| 2434 | + | // Allow navigation | |
| 2435 | + | return C.TRUE | |
| 2436 | + | } | |
| 2437 | + | ||
| 2373 | 2438 | // showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability | |
| 2374 | 2439 | func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool { | |
| 2375 | 2440 | if w == nil || w.native == nil || w.native.win == nil { | |
| @@ -80,7 +80,10 @@ static void on_webview_close(WebKitWebView* web_view, gpointer user_data) { | |||||
| 80 | } | 80 | } | ||
| 81 | 81 | ||||
| 82 | 82 | ||||
| 83 | - | // Handle decide-policy signal for new window requests - more robust than create signal | 83 | + | // Forward declare navigation policy handler |
| 84 | + | extern gboolean goHandleNavigationPolicy(unsigned long view_id, char* uri, gboolean is_user_gesture); | |||
| 85 | + | ||||
| 86 | + | // Handle decide-policy signal for new window requests and navigations | |||
| 84 | static gboolean on_decide_policy(WebKitWebView *web_view, | 87 | static gboolean on_decide_policy(WebKitWebView *web_view, | ||
| 85 | WebKitPolicyDecision *decision, | 88 | WebKitPolicyDecision *decision, | ||
| 86 | WebKitPolicyDecisionType type, | 89 | WebKitPolicyDecisionType type, | ||
| @@ -90,6 +93,32 @@ static gboolean on_decide_policy(WebKitWebView *web_view, | |||||
| 90 | const char* type_name = (type >= 0 && type < 3) ? type_names[type] : "UNKNOWN"; | 93 | const char* type_name = (type >= 0 && type < 3) ? type_names[type] : "UNKNOWN"; | ||
| 91 | printf("[webkit-policy] Policy decision: type=%s (%d)\n", type_name, type); | 94 | printf("[webkit-policy] Policy decision: type=%s (%d)\n", type_name, type); | ||
| 92 | 95 | ||||
| 96 | + | unsigned long view_id = (unsigned long)user_data; | |||
| 97 | + | ||||
| 98 | + | if (type == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { | |||
| 99 | + | WebKitNavigationPolicyDecision *nav_decision = | |||
| 100 | + | WEBKIT_NAVIGATION_POLICY_DECISION(decision); | |||
| 101 | + | WebKitNavigationAction *action = | |||
| 102 | + | webkit_navigation_policy_decision_get_navigation_action(nav_decision); | |||
| 103 | + | WebKitURIRequest *request = | |||
| 104 | + | webkit_navigation_action_get_request(action); | |||
| 105 | + | const char* uri = webkit_uri_request_get_uri(request); | |||
| 106 | + | gboolean is_user_gesture = webkit_navigation_action_is_user_gesture(action); | |||
| 107 | + | ||||
| 108 | + | // Ask Go if this navigation should be allowed | |||
| 109 | + | gboolean allowed = goHandleNavigationPolicy(view_id, (char*)uri, is_user_gesture); | |||
| 110 | + | ||||
| 111 | + | if (!allowed) { | |||
| 112 | + | printf("[webkit-policy] Navigation BLOCKED by Go handler: uri=%s is_user_gesture=%d\n", uri, is_user_gesture); | |||
| 113 | + | webkit_policy_decision_ignore(decision); | |||
| 114 | + | return TRUE; | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // Allow navigation | |||
| 118 | + | webkit_policy_decision_use(decision); | |||
| 119 | + | return TRUE; | |||
| 120 | + | } | |||
| 121 | + | ||||
| 93 | if (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { | 122 | if (type == WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION) { | ||
| 94 | WebKitNavigationPolicyDecision *nav_decision = | 123 | WebKitNavigationPolicyDecision *nav_decision = | ||
| 95 | WEBKIT_NAVIGATION_POLICY_DECISION(decision); | 124 | WEBKIT_NAVIGATION_POLICY_DECISION(decision); | ||
| @@ -111,7 +140,6 @@ static gboolean on_decide_policy(WebKitWebView *web_view, | |||||
| 111 | guint mouse_button = webkit_navigation_action_get_mouse_button(action); | 140 | guint mouse_button = webkit_navigation_action_get_mouse_button(action); | ||
| 112 | 141 | ||||
| 113 | // Call Go handler for popup creation | 142 | // Call Go handler for popup creation | ||
| 114 | - | unsigned long view_id = (unsigned long)user_data; | |||
| 115 | gboolean handled = goHandleNewWindowPolicy(view_id, (char*)uri, nav_type, (char*)frame_name, | 143 | gboolean handled = goHandleNewWindowPolicy(view_id, (char*)uri, nav_type, (char*)frame_name, | ||
| 116 | is_user_gesture, modifiers, mouse_button); | 144 | is_user_gesture, modifiers, mouse_button); | ||
| 117 | 145 | ||||
| @@ -248,9 +276,9 @@ static GtkWidget* new_webview_related(WebKitWebView* related_view, WebKitUserCon | |||||
| 248 | 276 | ||||
| 249 | static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) { | 277 | static void connect_policy_handler(WebKitWebView* web_view, unsigned long id) { | ||
| 250 | if (!web_view) return; | 278 | if (!web_view) return; | ||
| 251 | - | // Create signal is now handled in window_handler_cgo.go | 279 | + | // Connect decide-policy signal for navigation control |
| 252 | - | // Use ConnectCreateSignal from Go to connect the signal | 280 | + | g_signal_connect_data(G_OBJECT(web_view), "decide-policy", G_CALLBACK(on_decide_policy), (gpointer)id, NULL, 0); |
| 253 | - | printf("[webkit-debug] Policy handler connected (create signal handled separately)\n"); | 281 | + | printf("[webkit-debug] Policy handler connected for WebView id: %lu\n", id); |
| 254 | } | 282 | } | ||
| 255 | 283 | ||||
| 256 | // Connect close signal to a WebView for popup auto-close functionality | 284 | // Connect close signal to a WebView for popup auto-close functionality | ||
| @@ -987,12 +1015,13 @@ type WebView struct { | |||||
| 987 | wasReparented bool // Track if this WebView has been reparented | 1015 | wasReparented bool // Track if this WebView has been reparented | ||
| 988 | 1016 | ||||
| 989 | // Window type detection and relationships | 1017 | // Window type detection and relationships | ||
| 990 | - | windowType WindowType | 1018 | + | windowType WindowType |
| 991 | - | windowFeatures *WindowFeatures | 1019 | + | windowFeatures *WindowFeatures |
| 992 | - | windowTypeCallback func(WindowType, *WindowFeatures) | 1020 | + | windowTypeCallback func(WindowType, *WindowFeatures) |
| 993 | - | parentWebView *WebView | 1021 | + | parentWebView *WebView |
| 994 | - | isRelated bool | 1022 | + | isRelated bool |
| 995 | - | isActive bool // Track if this WebView is currently active/focused | 1023 | + | isActive bool // Track if this WebView is currently active/focused |
| 1024 | + | navigationPolicyHandler func(url string, isUserGesture bool) bool // Handler to decide if navigation should be allowed | |||
| 996 | } | 1025 | } | ||
| 997 | 1026 | ||||
| 998 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | 1027 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | ||
| @@ -1769,6 +1798,11 @@ func (w *WebView) RegisterCloseHandler(cb func()) { | |||||
| 1769 | } | 1798 | } | ||
| 1770 | } | 1799 | } | ||
| 1771 | 1800 | ||||
| 1801 | + | // RegisterNavigationPolicyHandler sets a handler to control navigation decisions | |||
| 1802 | + | func (w *WebView) RegisterNavigationPolicyHandler(cb func(url string, isUserGesture bool) bool) { | |||
| 1803 | + | w.navigationPolicyHandler = cb | |||
| 1804 | + | } | |||
| 1805 | + | ||||
| 1772 | // EnablePrintInterception connects the print signal to intercept Ctrl+P | 1806 | // EnablePrintInterception connects the print signal to intercept Ctrl+P | ||
| 1773 | func (w *WebView) EnablePrintInterception() { | 1807 | func (w *WebView) EnablePrintInterception() { | ||
| 1774 | if w.native != nil && w.native.wv != nil { | 1808 | if w.native != nil && w.native.wv != nil { | ||
| @@ -2370,6 +2404,37 @@ func goHandleConsoleMessage(message *C.char) { | |||||
| 2370 | logging.CaptureWebKitLog(messageText) | 2404 | logging.CaptureWebKitLog(messageText) | ||
| 2371 | } | 2405 | } | ||
| 2372 | 2406 | ||||
| 2407 | + | //export goHandleNavigationPolicy | |||
| 2408 | + | func goHandleNavigationPolicy(viewID C.ulong, uri *C.char, isUserGesture C.gboolean) C.gboolean { | |||
| 2409 | + | id := uintptr(viewID) | |||
| 2410 | + | urlStr := C.GoString(uri) | |||
| 2411 | + | userGesture := isUserGesture != 0 | |||
| 2412 | + | ||||
| 2413 | + | log.Printf("[navigation-policy] WebView %d navigating to: %s (user_gesture=%t)", id, urlStr, userGesture) | |||
| 2414 | + | ||||
| 2415 | + | // Find the WebView | |||
| 2416 | + | regMu.RLock() | |||
| 2417 | + | view, exists := viewByID[id] | |||
| 2418 | + | regMu.RUnlock() | |||
| 2419 | + | ||||
| 2420 | + | if !exists || view == nil { | |||
| 2421 | + | log.Printf("[navigation-policy] WebView %d not found, allowing navigation", id) | |||
| 2422 | + | return C.TRUE // Allow by default if view not found | |||
| 2423 | + | } | |||
| 2424 | + | ||||
| 2425 | + | // Check if this webview has a navigation policy handler | |||
| 2426 | + | if view.navigationPolicyHandler != nil { | |||
| 2427 | + | allowed := view.navigationPolicyHandler(urlStr, userGesture) | |||
| 2428 | + | if !allowed { | |||
| 2429 | + | log.Printf("[navigation-policy] WebView %d: Navigation BLOCKED by handler", id) | |||
| 2430 | + | return C.FALSE | |||
| 2431 | + | } | |||
| 2432 | + | } | |||
| 2433 | + | ||||
| 2434 | + | // Allow navigation | |||
| 2435 | + | return C.TRUE | |||
| 2436 | + | } | |||
| 2437 | + | ||||
| 2373 | // showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability | 2438 | // showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability | ||
| 2374 | func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool { | 2439 | func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool { | ||
| 2375 | if w == nil || w.native == nil || w.native.win == nil { | 2440 | if w == nil || w.native == nil || w.native.win == nil { | ||
M pkg/webkit/workspace_widgets_cgo.go +32 -0
| @@ -586,6 +586,14 @@ func PanedSetPosition(paned uintptr, pos int) { | |||
| 586 | 586 | C.gtk_paned_set_position((*C.GtkPaned)(unsafe.Pointer(paned)), C.int(pos)) | |
| 587 | 587 | } | |
| 588 | 588 | ||
| 589 | + | // PanedGetPosition gets the current position of the divider in a GtkPaned | |
| 590 | + | func PanedGetPosition(paned uintptr) int { | |
| 591 | + | if paned == 0 { | |
| 592 | + | return 0 | |
| 593 | + | } | |
| 594 | + | return int(C.gtk_paned_get_position((*C.GtkPaned)(unsafe.Pointer(paned)))) | |
| 595 | + | } | |
| 596 | + | ||
| 589 | 597 | // WidgetUnparent detaches a widget from its current parent. | |
| 590 | 598 | func WidgetUnparent(widget uintptr) { | |
| 591 | 599 | if ok := WidgetUnparentChecked(widget); !ok { | |
| @@ -661,6 +669,16 @@ func WidgetSetVExpand(widget uintptr, expand bool) { | |||
| 661 | 669 | C.gtk_widget_set_vexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand)) | |
| 662 | 670 | } | |
| 663 | 671 | ||
| 672 | + | // WidgetSetSizeRequest sets the minimum size request for a widget. | |
| 673 | + | // Use -1 for width or height to unset that dimension. | |
| 674 | + | func WidgetSetSizeRequest(widget uintptr, width, height int) { | |
| 675 | + | if widget == 0 || !widgetIsValid(widget) { | |
| 676 | + | return | |
| 677 | + | } | |
| 678 | + | log.Printf("[workspace] WidgetSetSizeRequest widget=%#x width=%d height=%d", widget, width, height) | |
| 679 | + | C.gtk_widget_set_size_request((*C.GtkWidget)(unsafe.Pointer(widget)), C.int(width), C.int(height)) | |
| 680 | + | } | |
| 681 | + | ||
| 664 | 682 | // WidgetResetSizeRequest clears explicit size constraints so GTK can recalculate allocation. | |
| 665 | 683 | func WidgetResetSizeRequest(widget uintptr) { | |
| 666 | 684 | if widget == 0 || !widgetIsValid(widget) { | |
| @@ -737,6 +755,20 @@ func WidgetQueueAllocate(widget uintptr) { | |||
| 737 | 755 | C.gtk_widget_queue_allocate((*C.GtkWidget)(unsafe.Pointer(widget))) | |
| 738 | 756 | } | |
| 739 | 757 | ||
| 758 | + | // WidgetQueueResize forces GTK to recalculate the widget's layout and size allocation. | |
| 759 | + | // This is useful when a widget's size requirements have changed (e.g., after adding children). | |
| 760 | + | func WidgetQueueResize(widget uintptr) { | |
| 761 | + | if widget == 0 { | |
| 762 | + | return | |
| 763 | + | } | |
| 764 | + | if !widgetIsValid(widget) { | |
| 765 | + | log.Printf("[workspace] WidgetQueueResize skipped invalid widget=%#x", widget) | |
| 766 | + | return | |
| 767 | + | } | |
| 768 | + | log.Printf("[workspace] WidgetQueueResize widget=%#x", widget) | |
| 769 | + | C.gtk_widget_queue_resize((*C.GtkWidget)(unsafe.Pointer(widget))) | |
| 770 | + | } | |
| 771 | + | ||
| 740 | 772 | // WidgetRealizeInContainer ensures a widget is properly realized within a container | |
| 741 | 773 | func WidgetRealizeInContainer(widget uintptr) { | |
| 742 | 774 | if widget == 0 { | |
| @@ -586,6 +586,14 @@ func PanedSetPosition(paned uintptr, pos int) { | |||||
| 586 | C.gtk_paned_set_position((*C.GtkPaned)(unsafe.Pointer(paned)), C.int(pos)) | 586 | C.gtk_paned_set_position((*C.GtkPaned)(unsafe.Pointer(paned)), C.int(pos)) | ||
| 587 | } | 587 | } | ||
| 588 | 588 | ||||
| 589 | + | // PanedGetPosition gets the current position of the divider in a GtkPaned | |||
| 590 | + | func PanedGetPosition(paned uintptr) int { | |||
| 591 | + | if paned == 0 { | |||
| 592 | + | return 0 | |||
| 593 | + | } | |||
| 594 | + | return int(C.gtk_paned_get_position((*C.GtkPaned)(unsafe.Pointer(paned)))) | |||
| 595 | + | } | |||
| 596 | + | ||||
| 589 | // WidgetUnparent detaches a widget from its current parent. | 597 | // WidgetUnparent detaches a widget from its current parent. | ||
| 590 | func WidgetUnparent(widget uintptr) { | 598 | func WidgetUnparent(widget uintptr) { | ||
| 591 | if ok := WidgetUnparentChecked(widget); !ok { | 599 | if ok := WidgetUnparentChecked(widget); !ok { | ||
| @@ -661,6 +669,16 @@ func WidgetSetVExpand(widget uintptr, expand bool) { | |||||
| 661 | C.gtk_widget_set_vexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand)) | 669 | C.gtk_widget_set_vexpand((*C.GtkWidget)(unsafe.Pointer(widget)), goBool(expand)) | ||
| 662 | } | 670 | } | ||
| 663 | 671 | ||||
| 672 | + | // WidgetSetSizeRequest sets the minimum size request for a widget. | |||
| 673 | + | // Use -1 for width or height to unset that dimension. | |||
| 674 | + | func WidgetSetSizeRequest(widget uintptr, width, height int) { | |||
| 675 | + | if widget == 0 || !widgetIsValid(widget) { | |||
| 676 | + | return | |||
| 677 | + | } | |||
| 678 | + | log.Printf("[workspace] WidgetSetSizeRequest widget=%#x width=%d height=%d", widget, width, height) | |||
| 679 | + | C.gtk_widget_set_size_request((*C.GtkWidget)(unsafe.Pointer(widget)), C.int(width), C.int(height)) | |||
| 680 | + | } | |||
| 681 | + | ||||
| 664 | // WidgetResetSizeRequest clears explicit size constraints so GTK can recalculate allocation. | 682 | // WidgetResetSizeRequest clears explicit size constraints so GTK can recalculate allocation. | ||
| 665 | func WidgetResetSizeRequest(widget uintptr) { | 683 | func WidgetResetSizeRequest(widget uintptr) { | ||
| 666 | if widget == 0 || !widgetIsValid(widget) { | 684 | if widget == 0 || !widgetIsValid(widget) { | ||
| @@ -737,6 +755,20 @@ func WidgetQueueAllocate(widget uintptr) { | |||||
| 737 | C.gtk_widget_queue_allocate((*C.GtkWidget)(unsafe.Pointer(widget))) | 755 | C.gtk_widget_queue_allocate((*C.GtkWidget)(unsafe.Pointer(widget))) | ||
| 738 | } | 756 | } | ||
| 739 | 757 | ||||
| 758 | + | // WidgetQueueResize forces GTK to recalculate the widget's layout and size allocation. | |||
| 759 | + | // This is useful when a widget's size requirements have changed (e.g., after adding children). | |||
| 760 | + | func WidgetQueueResize(widget uintptr) { | |||
| 761 | + | if widget == 0 { | |||
| 762 | + | return | |||
| 763 | + | } | |||
| 764 | + | if !widgetIsValid(widget) { | |||
| 765 | + | log.Printf("[workspace] WidgetQueueResize skipped invalid widget=%#x", widget) | |||
| 766 | + | return | |||
| 767 | + | } | |||
| 768 | + | log.Printf("[workspace] WidgetQueueResize widget=%#x", widget) | |||
| 769 | + | C.gtk_widget_queue_resize((*C.GtkWidget)(unsafe.Pointer(widget))) | |||
| 770 | + | } | |||
| 771 | + | ||||
| 740 | // WidgetRealizeInContainer ensures a widget is properly realized within a container | 772 | // WidgetRealizeInContainer ensures a widget is properly realized within a container | ||
| 741 | func WidgetRealizeInContainer(widget uintptr) { | 773 | func WidgetRealizeInContainer(widget uintptr) { | ||
| 742 | if widget == 0 { | 774 | if widget == 0 { | ||
M pkg/webkit/workspace_widgets_stub.go +7 -0
| @@ -199,6 +199,9 @@ func PanedSetResizeEnd(paned uintptr, resize bool) {} | |||
| 199 | 199 | // PanedSetPosition is a no-op in stub builds. | |
| 200 | 200 | func PanedSetPosition(paned uintptr, pos int) {} | |
| 201 | 201 | ||
| 202 | + | // PanedGetPosition gets the current position of the divider in a GtkPaned (stub) | |
| 203 | + | func PanedGetPosition(paned uintptr) int { return 0 } | |
| 204 | + | ||
| 202 | 205 | func WidgetUnparent(widget uintptr) { | |
| 203 | 206 | widgetMu.Lock() | |
| 204 | 207 | defer widgetMu.Unlock() | |
| @@ -289,8 +292,12 @@ func WidgetQueueAllocate(widget uintptr) {} | |||
| 289 | 292 | ||
| 290 | 293 | func WidgetRealizeInContainer(widget uintptr) {} | |
| 291 | 294 | ||
| 295 | + | func WidgetSetSizeRequest(widget uintptr, width, height int) {} | |
| 296 | + | ||
| 292 | 297 | func WidgetResetSizeRequest(widget uintptr) {} | |
| 293 | 298 | ||
| 299 | + | func WidgetQueueResize(widget uintptr) {} | |
| 300 | + | ||
| 294 | 301 | func WidgetHookDestroy(widget uintptr) {} | |
| 295 | 302 | ||
| 296 | 303 | func WidgetIsValid(widget uintptr) bool { | |
| @@ -199,6 +199,9 @@ func PanedSetResizeEnd(paned uintptr, resize bool) {} | |||||
| 199 | // PanedSetPosition is a no-op in stub builds. | 199 | // PanedSetPosition is a no-op in stub builds. | ||
| 200 | func PanedSetPosition(paned uintptr, pos int) {} | 200 | func PanedSetPosition(paned uintptr, pos int) {} | ||
| 201 | 201 | ||||
| 202 | + | // PanedGetPosition gets the current position of the divider in a GtkPaned (stub) | |||
| 203 | + | func PanedGetPosition(paned uintptr) int { return 0 } | |||
| 204 | + | ||||
| 202 | func WidgetUnparent(widget uintptr) { | 205 | func WidgetUnparent(widget uintptr) { | ||
| 203 | widgetMu.Lock() | 206 | widgetMu.Lock() | ||
| 204 | defer widgetMu.Unlock() | 207 | defer widgetMu.Unlock() | ||
| @@ -289,8 +292,12 @@ func WidgetQueueAllocate(widget uintptr) {} | |||||
| 289 | 292 | ||||
| 290 | func WidgetRealizeInContainer(widget uintptr) {} | 293 | func WidgetRealizeInContainer(widget uintptr) {} | ||
| 291 | 294 | ||||
| 295 | + | func WidgetSetSizeRequest(widget uintptr, width, height int) {} | |||
| 296 | + | ||||
| 292 | func WidgetResetSizeRequest(widget uintptr) {} | 297 | func WidgetResetSizeRequest(widget uintptr) {} | ||
| 293 | 298 | ||||
| 299 | + | func WidgetQueueResize(widget uintptr) {} | |||
| 300 | + | ||||
| 294 | func WidgetHookDestroy(widget uintptr) {} | 301 | func WidgetHookDestroy(widget uintptr) {} | ||
| 295 | 302 | ||||
| 296 | func WidgetIsValid(widget uintptr) bool { | 303 | func WidgetIsValid(widget uintptr) bool { | ||