1// workspace_popup.go - Popup window handling and related pane management
2package browser
3
4import (
5 "fmt"
6 "strings"
7 "time"
8
9 "github.com/bnema/dumber/internal/config"
10 "github.com/bnema/dumber/internal/logging"
11 "github.com/bnema/dumber/pkg/webkit"
12 webkitgtk "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
13)
14
15const (
16 // Default popup window dimensions - sized for OAuth flows
17 defaultPopupMinWidth = 500 // Minimum width for OAuth popups
18 defaultPopupMinHeight = 600 // Minimum height for OAuth popups
19)
20
21// HandlePopup is DEPRECATED and removed.
22// Popup lifecycle now uses WebKit's native create/ready-to-show/close signals.
23// See setupPopupHandling(), handlePopupReadyToShow(), and handlePopupClose() instead.
24
25// registerOAuthAutoClose sets up OAuth auto-close functionality for popups
26// Note: OAuth detection is now handled by the main-world.js injection script
27func (wm *WorkspaceManager) registerOAuthAutoClose(view *webkit.WebView, url string) {
28 logging.Info(fmt.Sprintf("[workspace] OAuth auto-close enabled for popup with URL: %s", url))
29 logging.Info(fmt.Sprintf("[workspace] OAuth detection will be handled by main-world.js injection script"))
30}
31
32// insertIndependentPane inserts a new independent pane next to the source
33func (wm *WorkspaceManager) insertIndependentPane(sourceNode *paneNode, webView *webkit.WebView, url string) error {
34 newPane, err := wm.createPane(webView)
35 if err != nil {
36 return err
37 }
38 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
39 if direction == "" {
40 direction = DirectionRight
41 }
42 // Use splitNode with existing pane instead of insertPopupPane
43 if _, err := wm.splitNode(sourceNode, direction, newPane); err != nil {
44 return err
45 }
46 node := wm.viewToNode[webView]
47 if node != nil {
48 node.windowType = webkit.WindowTypeTab
49 node.isRelated = false
50 }
51 if url != "" {
52 _ = webView.LoadURL(url)
53 }
54 return nil
55}
56
57// configureRelatedPopup creates a related view and inserts it
58func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView *webkit.WebView, url string, feat *webkit.WindowFeatures) {
59 // Use the WebView that was already created and returned to WebKit
60 related := webView
61 newPane, err := wm.createPane(related)
62 if err != nil {
63 logging.Error(fmt.Sprintf("[workspace] failed to create related popup pane: %v", err))
64 return
65 }
66 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
67 if direction == "" {
68 direction = DirectionRight
69 }
70 // Use splitNode with existing pane instead of insertPopupPane
71 if _, err := wm.splitNode(sourceNode, direction, newPane); err != nil {
72 logging.Error(fmt.Sprintf("[workspace] failed to insert related popup pane: %v", err))
73 return
74 }
75 node := wm.viewToNode[related]
76 popupWebViewID := related.IDString()
77
78 if node != nil {
79 node.windowType = webkit.WindowTypePopup
80 node.windowFeatures = feat
81 node.isRelated = true
82 node.parentPane = sourceNode
83 node.isPopup = true
84 // Heuristic + config for auto-close intent
85 node.autoClose = wm.shouldAutoClose(url)
86
87 // Apply minimum size constraints to prevent compression
88 var width, height int
89 if feat != nil {
90 width = feat.Width
91 height = feat.Height
92 }
93 wm.applyPopupSizeConstraints(related, width, height)
94 }
95
96 // Track this popup in the parent's activePopupChildren
97 // This is critical to prevent OAuth flows from hijacking the parent pane via window.opener
98 if sourceNode != nil && popupWebViewID != "" {
99 if sourceNode.activePopupChildren == nil {
100 sourceNode.activePopupChildren = make([]string, 0)
101 }
102 sourceNode.activePopupChildren = append(sourceNode.activePopupChildren, popupWebViewID)
103 logging.Debug(fmt.Sprintf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)", popupWebViewID, len(sourceNode.activePopupChildren)))
104
105 // Register navigation policy handler for parent to block script-initiated navigations
106 // when it has active popup children (prevents OAuth from hijacking parent pane)
107 if sourceNode.pane != nil && sourceNode.pane.WebView() != nil {
108 sourceNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool {
109 // Always allow user-initiated navigations
110 if isUserGesture {
111 return true
112 }
113
114 // Block script-initiated navigations if this pane has active popup children
115 if len(sourceNode.activePopupChildren) > 0 {
116 logging.Info(fmt.Sprintf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s", len(sourceNode.activePopupChildren), url))
117 return false
118 }
119
120 // Allow if no active popups
121 return true
122 })
123 logging.Debug(fmt.Sprintf("[workspace] Registered navigation policy handler for parent pane"))
124 }
125 }
126
127 // Pipe into existing auto-close flow only for popups (confirmed by detection)
128 related.RegisterCloseHandler(func() {
129 logging.Info(fmt.Sprintf("[workspace] Popup requesting close via window.close()"))
130
131 // Remove this popup from parent's activePopupChildren
132 if sourceNode != nil && popupWebViewID != "" {
133 for i, childID := range sourceNode.activePopupChildren {
134 if childID == popupWebViewID {
135 sourceNode.activePopupChildren = append(sourceNode.activePopupChildren[:i], sourceNode.activePopupChildren[i+1:]...)
136 logging.Debug(fmt.Sprintf("[workspace] Removed popup %s from parent's activePopupChildren (remaining: %d)", popupWebViewID, len(sourceNode.activePopupChildren)))
137 break
138 }
139 }
140 }
141
142 if n := wm.viewToNode[related]; n != nil && n.isPopup {
143 time.AfterFunc(200*time.Millisecond, func() {
144 wm.scheduleIdleGuarded(func() bool {
145 if n == nil || !n.widgetValid {
146 return false
147 }
148 if err := wm.ClosePane(n); err != nil {
149 logging.Error(fmt.Sprintf("[workspace] Failed to close popup pane: %v", err))
150 }
151 return false
152 }, n)
153 })
154 }
155 })
156
157 // URL-based fallback: if providers don't call window.close(), auto-close on OAuth callback URLs
158 if node != nil && node.isPopup && node.autoClose {
159 wm.registerOAuthAutoClose(related, url)
160 }
161 if url != "" {
162 _ = related.LoadURL(url)
163 }
164}
165
166// applyPopupSizeConstraints applies minimum size constraints to prevent OAuth popup compression
167func (wm *WorkspaceManager) applyPopupSizeConstraints(view *webkit.WebView, width, height int) {
168 minWidth := defaultPopupMinWidth
169 minHeight := defaultPopupMinHeight
170
171 // Use provided dimensions if valid
172 if width > 0 {
173 minWidth = width
174 }
175 if height > 0 {
176 minHeight = height
177 }
178
179 // Apply to container
180 if container := view.RootWidget(); container != nil {
181 webkit.WidgetSetSizeRequest(container, minWidth, minHeight)
182 webkit.WidgetQueueResize(container)
183 logging.Debug(fmt.Sprintf("[workspace] Applied minimum size %dx%d to popup container=%p", minWidth, minHeight, container))
184 }
185
186 // Apply to WebView widget
187 if webViewWidget := view.Widget(); webViewWidget != nil {
188 webkit.WidgetSetSizeRequest(webViewWidget, minWidth, minHeight)
189 webkit.WidgetQueueResize(webViewWidget)
190 logging.Debug(fmt.Sprintf("[workspace] Applied minimum size %dx%d to popup webview=%p", minWidth, minHeight, webViewWidget))
191 }
192}
193
194// shouldAutoClose checks simple OAuth-like URL patterns and config flag
195func (wm *WorkspaceManager) shouldAutoClose(url string) bool {
196 logging.Info(fmt.Sprintf("[workspace] shouldAutoClose called for URL: %s", url))
197
198 if wm == nil || wm.app == nil || wm.app.config == nil {
199 logging.Error(fmt.Sprintf("[workspace] shouldAutoClose: missing config, returning true"))
200 return true
201 }
202 if !wm.app.config.Workspace.Popups.OAuthAutoClose {
203 logging.Info(fmt.Sprintf("[workspace] shouldAutoClose: OAuthAutoClose disabled in config, returning false"))
204 return false
205 }
206
207 u := strings.ToLower(url)
208 logging.Debug(fmt.Sprintf("[workspace] shouldAutoClose: checking lowercase URL: %s", u))
209
210 // RFC 6749 compliant OAuth 2.0 URL patterns
211 oauthPatterns := []string{
212 // Standard OAuth endpoints
213 "oauth", "authorize", "authorization",
214 // Standard callback/redirect patterns
215 "callback", "redirect", "auth/callback",
216 // OpenID Connect patterns
217 "oidc", "openid",
218 // Common OAuth parameter indicators
219 "response_type=", "client_id=", "redirect_uri=", "scope=", "state=",
220 // Standard OAuth response parameters
221 "code=", "access_token=", "id_token=", "token_type=",
222 // Error response parameters
223 "error=", "error_description=",
224 }
225
226 // Check for OAuth patterns in URL
227 for _, pattern := range oauthPatterns {
228 if strings.Contains(u, pattern) {
229 logging.Info(fmt.Sprintf("[workspace] shouldAutoClose: MATCHED pattern '%s' in URL, returning true", pattern))
230 return true
231 }
232 }
233
234 logging.Info(fmt.Sprintf("[workspace] shouldAutoClose: no OAuth patterns matched, returning false"))
235 return false
236}
237
238// RunOnUI schedules a function; here simply executes inline as GTK main loop is single-threaded
239func (wm *WorkspaceManager) RunOnUI(fn func()) {
240 if fn != nil {
241 fn()
242 }
243}
244
245// handleDetectedWindowType handles window type detection from smart detection path
246func (wm *WorkspaceManager) handleDetectedWindowType(sourceNode *paneNode, webView *webkit.WebView, url string, windowType webkit.WindowType, features *webkit.WindowFeatures) {
247 if wm.viewToNode[webView] != nil {
248 return // Already placed
249 }
250
251 logging.Info(fmt.Sprintf("[workspace] Smart detection result: type=%d url=%s", windowType, url))
252
253 switch windowType {
254 case webkit.WindowTypeTab:
255 // For tabs, create a NEW independent WebView (can't use the related one)
256 webkitCfg, err := wm.app.buildWebkitConfig()
257 if err != nil {
258 logging.Error(fmt.Sprintf("[workspace] failed to build webkit config for tab: %v", err))
259 return
260 }
261 // Tabs are embedded in workspace paned containers - no separate window needed
262 webkitCfg.CreateWindow = false
263
264 // Create independent WebView matching our standard tab insertion path
265 independentView, err := webkit.NewWebView(webkitCfg)
266 if err != nil {
267 logging.Error(fmt.Sprintf("[workspace] failed to create independent tab WebView: %v", err))
268 return
269 }
270
271 // Register with content blocking service for network/cosmetic filtering
272 wm.app.RegisterWebViewForFiltering(independentView)
273
274 // The related webView was just for detection - we don't use it for tabs
275 // Insert the new independent view as a tab
276 if err := wm.insertIndependentPane(sourceNode, independentView, url); err != nil {
277 logging.Error(fmt.Sprintf("[workspace] Failed to insert independent pane: %v", err))
278 }
279
280 case webkit.WindowTypePopup:
281 // For popups, use the related WebView we already created
282 wm.configureRelatedPopup(sourceNode, webView, url, features)
283 default:
284 // Fallback to popup behavior
285 wm.configureRelatedPopup(sourceNode, webView, url, features)
286 }
287}
288
289// setupPopupHandling connects WebView's create signal for popup lifecycle management
290// This implements the WebKit-native popup lifecycle as described in POPUP_REFACTORING_PLAN.md
291func (wm *WorkspaceManager) setupPopupHandling(webView *webkit.WebView, parentNode *paneNode) {
292 if webView == nil || parentNode == nil {
293 return
294 }
295
296 webView.RegisterPopupCreateHandler(func(navAction *webkitgtk.NavigationAction) *webkit.WebView {
297 // Get URL from navigation action
298 req := navAction.Request()
299 if req == nil {
300 logging.Info(fmt.Sprintf("[workspace] create signal: no request in navigation action"))
301 return nil
302 }
303 url := req.URI()
304
305 logging.Info(fmt.Sprintf("[workspace] create signal received for URL: %s", url))
306
307 // Check if popups are enabled in config
308 if wm.app.config != nil && !wm.app.config.Workspace.Popups.OpenInNewPane {
309 logging.Info(fmt.Sprintf("[workspace] Popup creation disabled in config - blocking popup"))
310 return nil
311 }
312
313 // CRITICAL: Create a BARE gotk4 WebView as RELATED to parent (no wrapping, no initialization)
314 // WebKit requires the related-view property to be set during construction for proper lifecycle
315 // This ensures session/cookie sharing and proper WindowFeatures initialization
316 // All initialization (wrapping, container, scripts) happens in ready-to-show
317 logging.Debug(fmt.Sprintf("[workspace] Step 1: Creating bare gotk4 WebView with related-view property"))
318 barePopupView := webkit.NewBareRelatedWebView(webView.GtkWebView())
319 if barePopupView == nil {
320 logging.Error(fmt.Sprintf("[workspace] Failed to create bare related popup WebView - blocking popup"))
321 return nil
322 }
323 logging.Debug(fmt.Sprintf("[workspace] Step 1 complete: bare related gotk4 WebView created=%p (parent=%p)", barePopupView, webView.GtkWebView()))
324
325 // Wrap the bare view immediately to get a proper ID
326 // BUT don't initialize it yet - that happens in ready-to-show
327 logging.Info(fmt.Sprintf("[workspace] Step 2: Wrapping bare view (no initialization)"))
328 wrappedView := webkit.WrapBareWebView(barePopupView)
329 if wrappedView == nil {
330 logging.Error(fmt.Sprintf("[workspace] Failed to wrap bare popup WebView - blocking popup"))
331 return nil
332 }
333
334 // Use the wrapper's ID for tracking
335 popupID := wrappedView.ID()
336 logging.Debug(fmt.Sprintf("[workspace] Step 2 complete: wrapped view created with ID=%d", popupID))
337
338 // Store wrapped view (not initialized) for initialization in ready-to-show
339 logging.Info(fmt.Sprintf("[workspace] Step 3: Storing in pendingPopups map"))
340 wm.pendingPopups[popupID] = &pendingPopup{
341 wrappedView: wrappedView,
342 parentView: webView,
343 parentNode: parentNode,
344 url: url,
345 }
346 logging.Info(fmt.Sprintf("[workspace] Step 3 complete: stored in pendingPopups"))
347
348 // Connect signals immediately BEFORE returning to WebKit
349 // This is safe because we're only connecting signal handlers, not accessing properties
350 // WebKit will configure WindowFeatures AFTER this callback returns, which is fine
351 logging.Info(fmt.Sprintf("[workspace] Step 4: Connecting signals immediately before returning to WebKit"))
352
353 // Connect ready-to-show signal
354 barePopupView.ConnectReadyToShow(func() {
355 logging.Info(fmt.Sprintf("[workspace] ready-to-show signal received for popup ID=%d", popupID))
356 wm.handlePopupReadyToShow(popupID)
357 })
358 logging.Debug(fmt.Sprintf("[workspace] Connected ready-to-show signal for popup ID=%d", popupID))
359
360 // Connect close handler
361 barePopupView.ConnectClose(func() {
362 logging.Info(fmt.Sprintf("[workspace] close signal received for popup ID=%d", popupID))
363 wm.handlePopupClose(popupID)
364 })
365 logging.Debug(fmt.Sprintf("[workspace] Connected close signal for popup ID=%d", popupID))
366
367 logging.Debug(fmt.Sprintf("[workspace] Step 4 complete: signals connected"))
368
369 logging.Debug(fmt.Sprintf("[workspace] Step 5: Returning wrapped view to WebKit (will extract bare view in ConnectCreate)"))
370
371 // Return the wrapped view - WebKit will extract the underlying gotk4 view and configure it
372 return wrappedView
373 })
374}
375
376// handlePopupReadyToShow inserts popup pane when WebKit says it's ready
377func (wm *WorkspaceManager) handlePopupReadyToShow(popupID uint64) {
378 info, ok := wm.pendingPopups[popupID]
379 if !ok {
380 logging.Warn(fmt.Sprintf("[workspace] ready-to-show for unknown popup ID: %d", popupID))
381 return
382 }
383 delete(wm.pendingPopups, popupID)
384
385 logging.Info(fmt.Sprintf("[workspace] ready-to-show received for popup ID=%d, starting full initialization", popupID))
386
387 // NOW it's safe to initialize (WebKit has configured WindowProperties)
388 // We already have the wrapped view from setupPopupHandling
389 wrappedView := info.wrappedView
390
391 // Build webkit config for initialization
392 webkitCfg, err := wm.app.buildWebkitConfig()
393 if err != nil {
394 logging.Error(fmt.Sprintf("[workspace] Failed to build webkit config for popup initialization: %v", err))
395 return
396 }
397 webkitCfg.CreateWindow = false // Embedded in workspace
398
399 // Initialize the wrapped view with full configuration
400 if err := wrappedView.InitializeFromBare(webkitCfg); err != nil {
401 logging.Error(fmt.Sprintf("[workspace] Failed to initialize popup WebView: %v", err))
402 return
403 }
404
405 // Register with content blocking service for network/cosmetic filtering
406 wm.app.RegisterWebViewForFiltering(wrappedView)
407
408 logging.Debug(fmt.Sprintf("[workspace] Successfully initialized popup WebView ID %d", wrappedView.ID()))
409
410 // Create pane for the fully initialized WebView
411 popupPane, err := wm.createPane(wrappedView)
412 if err != nil {
413 logging.Error(fmt.Sprintf("[workspace] Failed to create popup pane: %v", err))
414 return
415 }
416
417 logging.Info(fmt.Sprintf("[workspace] Created popup pane, now inserting into workspace"))
418
419 // Determine window type from node (set during setupPopupHandling)
420 node := wm.viewToNode[wrappedView]
421 windowType := webkit.WindowTypePopup
422 if node != nil {
423 windowType = node.windowType
424 }
425
426 // Select behavior based on window type:
427 // - WindowTypeTab (_blank links): Use BlankTargetBehavior config
428 // - WindowTypePopup (JavaScript popups): Use global Behavior config
429 var behaviorStr string
430 if windowType == webkit.WindowTypeTab {
431 behaviorStr = wm.app.config.Workspace.Popups.BlankTargetBehavior
432 logging.Debug(fmt.Sprintf("[workspace] WindowTypeTab (_blank link) detected, using BlankTargetBehavior: %s", behaviorStr))
433 } else {
434 behaviorStr = string(wm.app.config.Workspace.Popups.Behavior)
435 logging.Debug(fmt.Sprintf("[workspace] WindowTypePopup detected, using Behavior: %s", behaviorStr))
436 }
437
438 // Map string to PopupBehavior enum
439 var behavior config.PopupBehavior
440 switch behaviorStr {
441 case "split":
442 behavior = config.PopupBehaviorSplit
443 case "stacked":
444 behavior = config.PopupBehaviorStacked
445 case "tabbed":
446 behavior = config.PopupBehaviorTabbed
447 default:
448 behavior = config.PopupBehaviorSplit // Fallback
449 }
450
451 logging.Info(fmt.Sprintf("[workspace] Final behavior: %s", behavior))
452
453 switch behavior {
454 case config.PopupBehaviorSplit:
455 // Split pane behavior (default) - use splitNode with existing popup pane
456 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
457 if direction == "" {
458 direction = DirectionRight
459 }
460 if _, err := wm.splitNode(info.parentNode, direction, popupPane); err != nil {
461 logging.Error(fmt.Sprintf("[workspace] Failed to insert split popup pane: %v", err))
462 return
463 }
464
465 case config.PopupBehaviorStacked:
466 // Stacked pane behavior
467 _, err := wm.stackedPaneManager.StackPane(info.parentNode)
468 if err != nil {
469 logging.Error(fmt.Sprintf("[workspace] Failed to stack existing pane: %v", err))
470 return
471 }
472 // Insert the popup pane in the same container - use splitNode with existing popup pane
473 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
474 if direction == "" {
475 direction = DirectionRight
476 }
477 if _, err := wm.splitNode(info.parentNode, direction, popupPane); err != nil {
478 logging.Error(fmt.Sprintf("[workspace] Failed to insert stacked popup pane: %v", err))
479 return
480 }
481
482 case config.PopupBehaviorTabbed:
483 // Tabbed behavior - TODO: implement proper tabbed interface
484 logging.Warn(fmt.Sprintf("[workspace] WARNING: Tabbed popup behavior not yet implemented, falling back to split"))
485 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
486 if direction == "" {
487 direction = DirectionRight
488 }
489 if _, err := wm.splitNode(info.parentNode, direction, popupPane); err != nil {
490 logging.Error(fmt.Sprintf("[workspace] Failed to insert tabbed popup pane: %v", err))
491 return
492 }
493
494 case config.PopupBehaviorWindowed:
495 // Windowed behavior - opens in new workspace/window
496 // TODO: implement new window creation with GTK
497 logging.Warn(fmt.Sprintf("[workspace] WARNING: Windowed popup behavior not yet implemented, falling back to split"))
498 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
499 if direction == "" {
500 direction = DirectionRight
501 }
502 if _, err := wm.splitNode(info.parentNode, direction, popupPane); err != nil {
503 logging.Error(fmt.Sprintf("[workspace] Failed to insert windowed popup pane: %v", err))
504 return
505 }
506
507 default:
508 // Fallback to split if unknown behavior
509 logging.Warn(fmt.Sprintf("[workspace] Unknown popup behavior '%s', falling back to split", behavior))
510 direction := strings.ToLower(wm.app.config.Workspace.Popups.Placement)
511 if direction == "" {
512 direction = DirectionRight
513 }
514 if _, err := wm.splitNode(info.parentNode, direction, popupPane); err != nil {
515 logging.Error(fmt.Sprintf("[workspace] Failed to insert popup pane: %v", err))
516 return
517 }
518 }
519
520 // Configure the popup node
521 // Note: windowType should already be set correctly from setupPopupHandling
522 // Don't overwrite it here - preserve WindowTypeTab for _blank links
523 node = wm.viewToNode[wrappedView]
524 if node != nil {
525 node.isRelated = true
526 node.parentPane = info.parentNode
527 node.isPopup = true // Mark as popup to prevent browser exit when parent closes
528 node.autoClose = wm.shouldAutoClose(info.url)
529 node.popupID = popupID // Store popupID for close signal lookup
530
531 // Track this popup in the parent's activePopupChildren
532 if info.parentNode != nil {
533 popupWebViewID := wrappedView.IDString()
534 if info.parentNode.activePopupChildren == nil {
535 info.parentNode.activePopupChildren = make([]string, 0)
536 }
537 info.parentNode.activePopupChildren = append(info.parentNode.activePopupChildren, popupWebViewID)
538 logging.Debug(fmt.Sprintf("[workspace] Added popup %s to parent's activePopupChildren (count: %d)",
539 popupWebViewID, len(info.parentNode.activePopupChildren)))
540
541 // Register navigation policy handler for parent
542 if info.parentNode.pane != nil && info.parentNode.pane.WebView() != nil {
543 info.parentNode.pane.WebView().RegisterNavigationPolicyHandler(func(url string, isUserGesture bool) bool {
544 if isUserGesture {
545 return true
546 }
547 if len(info.parentNode.activePopupChildren) > 0 {
548 logging.Info(fmt.Sprintf("[workspace] BLOCKED script-initiated navigation in parent pane (has %d active popups): %s",
549 len(info.parentNode.activePopupChildren), url))
550 return false
551 }
552 return true
553 })
554 }
555 }
556
557 // URL-based auto-close for OAuth popups
558 if node.autoClose {
559 wm.registerOAuthAutoClose(wrappedView, info.url)
560 }
561 }
562
563 wm.ensureGUIInPane(popupPane)
564
565 // Widgets are already shown by splitNode - no need to show again
566 // GTK4 handles visibility automatically during reparenting
567
568 logging.Info(fmt.Sprintf("[workspace] Popup pane inserted and visible for URL: %s", info.url))
569}
570
571// handlePopupClose responds to WebKit's close signal
572func (wm *WorkspaceManager) handlePopupClose(popupID uint64) {
573 logging.Info(fmt.Sprintf("[workspace] close signal received for popup native=%d", popupID))
574
575 // Check if it's in pending (not yet shown)
576 if info, ok := wm.pendingPopups[popupID]; ok {
577 delete(wm.pendingPopups, popupID)
578 logging.Info(fmt.Sprintf("[workspace] Popup closed before ready-to-show, cleaning up"))
579 // Just remove from pending, WebKit will handle cleanup
580 _ = info // avoid unused variable
581 return
582 }
583
584 // Find pane by popupID (stored in node during ready-to-show)
585 var targetNode *paneNode
586 for _, node := range wm.viewToNode {
587 if node != nil && node.popupID == popupID {
588 targetNode = node
589 break
590 }
591 }
592
593 if targetNode == nil {
594 logging.Warn(fmt.Sprintf("[workspace] close signal for unknown popup ID=%d, already closed?", popupID))
595 return
596 }
597
598 // Remove from parent's activePopupChildren
599 // Find the wrapped WebView for this node to get its IDString
600 var wrappedViewID string
601 for webView, node := range wm.viewToNode {
602 if node == targetNode {
603 wrappedViewID = webView.IDString()
604 break
605 }
606 }
607
608 if targetNode.parentPane != nil && wrappedViewID != "" {
609 for i, childID := range targetNode.parentPane.activePopupChildren {
610 if childID == wrappedViewID {
611 targetNode.parentPane.activePopupChildren = append(
612 targetNode.parentPane.activePopupChildren[:i],
613 targetNode.parentPane.activePopupChildren[i+1:]...)
614 logging.Debug(fmt.Sprintf("[workspace] Removed popup from parent's activePopupChildren (remaining: %d)",
615 len(targetNode.parentPane.activePopupChildren)))
616 break
617 }
618 }
619 }
620
621 // Clean close via existing ClosePane (WebKit already stopped)
622 logging.Info(fmt.Sprintf("[workspace] Closing popup pane via ClosePane"))
623 if err := wm.ClosePane(targetNode); err != nil {
624 logging.Error(fmt.Sprintf("[workspace] Error closing popup pane: %v", err))
625 }
626}