1package webkit
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7 "strconv"
8 "sync"
9 "sync/atomic"
10
11 webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
12 "github.com/diamondburned/gotk4/pkg/gtk/v4"
13)
14
15var (
16 viewIDCounter uint64
17 viewRegistry = make(map[uint64]*WebView)
18 viewMu sync.RWMutex
19)
20
21// WebView wraps a WebKitGTK WebView
22type WebView struct {
23 view *webkit.WebView
24 container *gtk.Box // Container that holds the WebView
25 window *Window // Optional window (only if CreateWindow=true)
26 id uint64
27
28 // State
29 config *Config
30 destroyed bool
31 mu sync.RWMutex
32
33 // Event handlers
34 onScriptMessage func(string)
35 onTitleChanged func(string)
36 onURIChanged func(string)
37 onFaviconChanged func([]byte)
38 onFaviconURIChanged func(pageURI, faviconURI string)
39 onZoomChanged func(float64)
40 onPopupCreate func(*webkit.NavigationAction) *WebView // New WebKit create signal handler
41 onReadyToShow func() // WebKit ready-to-show signal handler
42 onClose func()
43 onNavigationPolicy func(url string, isUserGesture bool) bool
44 onWindowTypeDetected func(WindowType, *WindowFeatures)
45 onWorkspaceNavigation func(direction string) bool // Workspace pane navigation
46 onPaneModeShortcut func(action string) bool // Pane mode shortcuts (enter, actions, exit)
47 isPaneModeActive func() bool // Check if pane mode is active
48}
49
50// NewWebView creates a new WebView with the given configuration
51func NewWebView(cfg *Config) (*WebView, error) {
52 if cfg == nil {
53 cfg = GetDefaultConfig()
54 }
55
56 // Generate unique ID immediately
57 id := atomic.AddUint64(&viewIDCounter, 1)
58 log.Printf("[webkit] Generated WebView ID: %d (CreateWindow=%v)", id, cfg.CreateWindow)
59
60 InitMainThread()
61
62 // Initialize persistent session - REQUIRED, no ephemeral fallback
63 // This must be done BEFORE creating the WebView
64 if cfg.DataDir == "" || cfg.CacheDir == "" {
65 return nil, fmt.Errorf("data and cache directories are required for persistent storage")
66 }
67
68 if err := InitPersistentSession(cfg.DataDir, cfg.CacheDir); err != nil {
69 return nil, fmt.Errorf("failed to initialize persistent storage: %w", err)
70 }
71
72 // Create WebView - it will automatically use the persistent session
73 // Per WebKitGTK 6.0: newly created session becomes the default for all WebViews
74 wkView := webkit.NewWebView()
75 if wkView == nil {
76 return nil, ErrWebViewNotInitialized
77 }
78
79 // Verify the WebView is using the persistent session
80 viewSession := wkView.NetworkSession()
81 if viewSession == nil {
82 return nil, fmt.Errorf("WebView has no network session")
83 }
84 if viewSession.IsEphemeral() {
85 return nil, fmt.Errorf("WebView is using ephemeral session despite persistent session initialization")
86 }
87
88 // CRITICAL: Configure CookieManager on the WebView's actual session
89 // This ensures cookies are persisted regardless of whether the WebView
90 // uses our global session or creates its own
91 cookieManager := viewSession.CookieManager()
92 if cookieManager == nil {
93 return nil, fmt.Errorf("failed to get cookie manager from WebView's network session")
94 }
95 cookiePath := cfg.DataDir + "/cookies.db"
96 cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqlite)
97 cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty)
98
99 // Setup UserContentManager and inject GUI scripts (including webview ID)
100 // This must be done BEFORE any pages are loaded
101 if err := SetupUserContentManager(wkView, cfg.AppearanceConfigJSON, id); err != nil {
102 return nil, fmt.Errorf("failed to setup user content manager: %w", err)
103 }
104
105 // Create container (GtkBox) to hold the WebView
106 // This allows the WebView to be reparented into workspace panes
107 container := gtk.NewBox(gtk.OrientationVertical, 0)
108 container.SetHExpand(true)
109 container.SetVExpand(true)
110
111 // Configure WebView widget for expansion
112 wkView.SetHExpand(true)
113 wkView.SetVExpand(true)
114
115 // Add WebView to container
116 container.Append(wkView)
117
118 wv := &WebView{
119 view: wkView,
120 container: container,
121 window: nil, // Will be set below if CreateWindow is true
122 id: id,
123 config: cfg,
124 }
125
126 // Apply configuration
127 if err := wv.applyConfig(); err != nil {
128 return nil, err
129 }
130
131 // Setup event handlers
132 wv.setupEventHandlers()
133
134 // Attach keyboard bridge to forward keyboard events to JavaScript
135 wv.AttachKeyboardBridge()
136
137 // Only create window if requested (standalone WebViews)
138 // Workspace panes will set CreateWindow=false and manage the container themselves
139 if cfg.CreateWindow {
140 window, err := NewWindow("Dumber Browser")
141 if err != nil {
142 return nil, err
143 }
144 wv.window = window
145 // Add container as child of window
146 window.SetChild(container)
147 log.Printf("[webkit] Created standalone window for WebView ID %d", id)
148 } else {
149 log.Printf("[webkit] Created WebView ID %d without window (for workspace embedding)", id)
150 }
151
152 // Register in global registry
153 viewMu.Lock()
154 viewRegistry[id] = wv
155 viewMu.Unlock()
156
157 return wv, nil
158}
159
160// applyConfig applies the configuration to the WebView settings
161func (w *WebView) applyConfig() error {
162 settings := w.view.Settings()
163 if settings == nil {
164 return fmt.Errorf("webkit: failed to get settings")
165 }
166
167 // Apply settings from config
168 settings.SetEnableJavascript(w.config.EnableJavaScript)
169 settings.SetEnableWebgl(w.config.EnableWebGL)
170 settings.SetDefaultFontSize(uint32(w.config.DefaultFontSize))
171 settings.SetMinimumFontSize(uint32(w.config.MinimumFontSize))
172
173 if w.config.UserAgent != "" {
174 settings.SetUserAgent(w.config.UserAgent)
175 }
176
177 // Enable developer tools (F12, inspector)
178 settings.SetEnableDeveloperExtras(true)
179
180 // Enable hardware acceleration if configured
181 settings.SetHardwareAccelerationPolicy(webkit.HardwareAccelerationPolicyAlways)
182
183 return nil
184}
185
186// setupEventHandlers connects GTK signals to internal handlers
187func (w *WebView) setupEventHandlers() {
188 // Title changed - connect to notify::title signal
189 w.view.Connect("notify::title", func() {
190 if w.onTitleChanged != nil {
191 title := w.view.Title()
192 w.onTitleChanged(title)
193 }
194 })
195
196 // URI changed - connect to notify::uri signal
197 w.view.Connect("notify::uri", func() {
198 if w.onURIChanged != nil {
199 uri := w.view.URI()
200 w.onURIChanged(uri)
201 }
202 })
203
204 // Zoom level changed - connect to notify::zoom-level signal
205 w.view.Connect("notify::zoom-level", func() {
206 if w.onZoomChanged != nil {
207 zoomLevel := w.view.ZoomLevel()
208 w.onZoomChanged(zoomLevel)
209 }
210 })
211
212 // Favicon changed - connect to FaviconDatabase
213 w.setupFaviconHandlers()
214
215 // Load changed
216 w.view.ConnectLoadChanged(func(event webkit.LoadEvent) {
217 // Handle load events if needed
218 })
219
220 // TLS error handling - connect to load-failed-with-tls-errors signal
221 w.setupTLSErrorHandler()
222
223 // Close
224 w.view.ConnectClose(func() {
225 if w.onClose != nil {
226 w.onClose()
227 }
228 })
229
230 // Create signal - for popup lifecycle management
231 w.view.ConnectCreate(func(navigationAction *webkit.NavigationAction) gtk.Widgetter {
232 log.Printf("[webkit] ConnectCreate callback fired for parent WebView ID=%d", w.id)
233 if w.onPopupCreate != nil {
234 log.Printf("[webkit] Calling onPopupCreate handler")
235 newWebView := w.onPopupCreate(navigationAction)
236 if newWebView != nil {
237 log.Printf("[webkit] onPopupCreate returned WebView wrapper ID=%d", newWebView.id)
238 log.Printf("[webkit] Extracting underlying gotk4 WebView from wrapper")
239 underlyingView := newWebView.view
240 log.Printf("[webkit] Extracted gotk4 WebView=%p, about to return to WebKit", underlyingView)
241 // Return the underlying WebView widget to WebKit
242 return underlyingView
243 }
244 log.Printf("[webkit] onPopupCreate returned nil, blocking popup")
245 }
246 // Return nil to cancel popup creation
247 log.Printf("[webkit] No onPopupCreate handler, blocking popup")
248 return nil
249 })
250
251 // Ready-to-show signal - emitted when popup is ready to be displayed
252 w.view.ConnectReadyToShow(func() {
253 if w.onReadyToShow != nil {
254 w.onReadyToShow()
255 }
256 })
257
258 // Script message received - connect to UserContentManager's script-message-received signal
259 // This receives messages from JavaScript via webkit.messageHandlers.dumber.postMessage()
260 // The signal name includes the handler name as a detail: "script-message-received::dumber"
261 ucm := w.view.UserContentManager()
262 if ucm != nil {
263 // GTK signals pass the sender as the first parameter, the actual signal data as subsequent parameters
264 ucm.Connect("script-message-received::dumber", func(sender interface{}, jscValue interface{}) {
265 if w.onScriptMessage != nil && jscValue != nil {
266 // Convert JSCValue to string using gotk4 javascriptcore bindings
267 valueStr := JSCValueToString(jscValue)
268 if valueStr != "" {
269 w.onScriptMessage(valueStr)
270 }
271 }
272 })
273 }
274}
275
276// LoadURL loads the given URL in the WebView
277func (w *WebView) LoadURL(url string) error {
278 w.mu.RLock()
279 defer w.mu.RUnlock()
280
281 if w.destroyed {
282 return ErrWebViewDestroyed
283 }
284
285 if url == "" {
286 return ErrInvalidURL
287 }
288
289 w.view.LoadURI(url)
290 return nil
291}
292
293// GetCurrentURL returns the current URL
294func (w *WebView) GetCurrentURL() string {
295 w.mu.RLock()
296 defer w.mu.RUnlock()
297
298 if w.destroyed {
299 return ""
300 }
301
302 return w.view.URI()
303}
304
305// GetTitle returns the current page title
306func (w *WebView) GetTitle() string {
307 w.mu.RLock()
308 defer w.mu.RUnlock()
309
310 if w.destroyed {
311 return ""
312 }
313
314 return w.view.Title()
315}
316
317// GoBack navigates back in history
318func (w *WebView) GoBack() error {
319 w.mu.RLock()
320 defer w.mu.RUnlock()
321
322 if w.destroyed {
323 return ErrWebViewDestroyed
324 }
325
326 w.view.GoBack()
327 return nil
328}
329
330// GoForward navigates forward in history
331func (w *WebView) GoForward() error {
332 w.mu.RLock()
333 defer w.mu.RUnlock()
334
335 if w.destroyed {
336 return ErrWebViewDestroyed
337 }
338
339 w.view.GoForward()
340 return nil
341}
342
343// Reload reloads the current page
344func (w *WebView) Reload() error {
345 w.mu.RLock()
346 defer w.mu.RUnlock()
347
348 if w.destroyed {
349 return ErrWebViewDestroyed
350 }
351
352 w.view.Reload()
353 return nil
354}
355
356// ReloadBypassCache reloads the current page, bypassing cache
357func (w *WebView) ReloadBypassCache() error {
358 w.mu.RLock()
359 defer w.mu.RUnlock()
360
361 if w.destroyed {
362 return ErrWebViewDestroyed
363 }
364
365 w.view.ReloadBypassCache()
366 return nil
367}
368
369// Show makes the WebView visible
370func (w *WebView) Show() error {
371 w.mu.RLock()
372 defer w.mu.RUnlock()
373
374 if w.destroyed {
375 return ErrWebViewDestroyed
376 }
377
378 // Make the WebView widget visible
379 w.view.SetVisible(true)
380
381 // Show the window containing the WebView
382 if w.window != nil {
383 w.window.Show()
384 }
385
386 return nil
387}
388
389// Hide makes the WebView invisible
390func (w *WebView) Hide() error {
391 w.mu.RLock()
392 defer w.mu.RUnlock()
393
394 if w.destroyed {
395 return ErrWebViewDestroyed
396 }
397
398 w.view.SetVisible(false)
399 return nil
400}
401
402// Destroy destroys the WebView and releases resources
403func (w *WebView) Destroy() error {
404 w.mu.Lock()
405 defer w.mu.Unlock()
406
407 if w.destroyed {
408 return nil
409 }
410
411 w.destroyed = true
412
413 // Unregister from global registry
414 viewMu.Lock()
415 delete(viewRegistry, w.id)
416 viewMu.Unlock()
417
418 // The GTK widget will be cleaned up by Go GC
419 return nil
420}
421
422// IsDestroyed returns true if the WebView has been destroyed
423func (w *WebView) IsDestroyed() bool {
424 w.mu.RLock()
425 defer w.mu.RUnlock()
426 return w.destroyed
427}
428
429// ID returns the unique identifier for this WebView
430func (w *WebView) ID() uint64 {
431 return w.id
432}
433
434// IDString returns the WebView ID as a string
435// This is a convenience method for compatibility with code expecting string IDs
436func (w *WebView) IDString() string {
437 return fmt.Sprintf("%d", w.id)
438}
439
440// ParseWebViewID converts a string WebView ID to uint64
441// Returns 0 if the string cannot be parsed
442func ParseWebViewID(idStr string) uint64 {
443 id, err := strconv.ParseUint(idStr, 10, 64)
444 if err != nil {
445 return 0
446 }
447 return id
448}
449
450// AsWidget returns the WebView as a gtk.Widgetter
451func (w *WebView) AsWidget() gtk.Widgetter {
452 if w == nil || w.view == nil {
453 return nil
454 }
455 return w.view
456}
457
458// Event handler registration methods
459
460// RegisterScriptMessageHandler registers a handler for script messages
461func (w *WebView) RegisterScriptMessageHandler(handler func(string)) {
462 w.mu.Lock()
463 defer w.mu.Unlock()
464 w.onScriptMessage = handler
465}
466
467// RegisterTitleChangedHandler registers a handler for title changes
468func (w *WebView) RegisterTitleChangedHandler(handler func(string)) {
469 w.mu.Lock()
470 defer w.mu.Unlock()
471 w.onTitleChanged = handler
472}
473
474// RegisterURIChangedHandler registers a handler for URI changes
475func (w *WebView) RegisterURIChangedHandler(handler func(string)) {
476 w.mu.Lock()
477 defer w.mu.Unlock()
478 w.onURIChanged = handler
479}
480
481// RegisterFaviconChangedHandler registers a handler for favicon changes
482func (w *WebView) RegisterFaviconChangedHandler(handler func([]byte)) {
483 w.mu.Lock()
484 defer w.mu.Unlock()
485 w.onFaviconChanged = handler
486}
487
488// RegisterFaviconURIChangedHandler registers a handler for favicon URI changes
489func (w *WebView) RegisterFaviconURIChangedHandler(handler func(pageURI, faviconURI string)) {
490 w.mu.Lock()
491 defer w.mu.Unlock()
492 w.onFaviconURIChanged = handler
493}
494
495// RegisterZoomChangedHandler registers a handler for zoom changes
496func (w *WebView) RegisterZoomChangedHandler(handler func(float64)) {
497 w.mu.Lock()
498 defer w.mu.Unlock()
499 w.onZoomChanged = handler
500}
501
502// RegisterCloseHandler registers a handler for close requests
503func (w *WebView) RegisterCloseHandler(handler func()) {
504 w.mu.Lock()
505 defer w.mu.Unlock()
506 w.onClose = handler
507}
508
509// RegisterPopupCreateHandler registers a handler for WebKit's create signal
510// This is called when JavaScript calls window.open() or a link with target="_blank" is clicked
511// The handler should create and return a new WebView for the popup, or return nil to block it
512// The returned WebView should NOT be shown yet - WebKit will emit ready-to-show when it's ready
513func (w *WebView) RegisterPopupCreateHandler(handler func(*webkit.NavigationAction) *WebView) {
514 w.mu.Lock()
515 defer w.mu.Unlock()
516 w.onPopupCreate = handler
517}
518
519// RegisterReadyToShowHandler registers a handler for WebKit's ready-to-show signal
520// This is called when a popup WebView is ready to be displayed
521// At this point it's safe to insert the popup into the UI (workspace, window, etc.)
522func (w *WebView) RegisterReadyToShowHandler(handler func()) {
523 w.mu.Lock()
524 defer w.mu.Unlock()
525 w.onReadyToShow = handler
526}
527
528// RegisterNavigationPolicyHandler registers a handler for navigation policy decisions
529func (w *WebView) RegisterNavigationPolicyHandler(handler func(url string, isUserGesture bool) bool) {
530 w.mu.Lock()
531 defer w.mu.Unlock()
532 w.onNavigationPolicy = handler
533}
534
535// OnWindowTypeDetected registers a handler for window type detection
536func (w *WebView) OnWindowTypeDetected(handler func(WindowType, *WindowFeatures)) {
537 w.mu.Lock()
538 defer w.mu.Unlock()
539 w.onWindowTypeDetected = handler
540}
541
542// RunOnMainThread executes a function on the GTK main thread
543func (w *WebView) RunOnMainThread(fn func()) {
544 RunOnMainThread(fn)
545}
546
547// GetWebView returns the underlying webkit.WebView for advanced operations
548func (w *WebView) GetWebView() *webkit.WebView {
549 return w.view
550}
551
552// Widget returns the WebView widget (for compatibility with old code expecting uintptr)
553// Deprecated: Use AsWidget() instead
554func (w *WebView) Widget() gtk.Widgetter {
555 return w.AsWidget()
556}
557
558// RootWidget returns the root container widget for this WebView
559// This is the GtkBox container that holds the WebView and can be reparented
560func (w *WebView) RootWidget() gtk.Widgetter {
561 if w.container != nil {
562 return w.container
563 }
564 // Fallback to WebView if container is not available (shouldn't happen)
565 return w.AsWidget()
566}
567
568// SetZoom sets the zoom level of the WebView
569func (w *WebView) SetZoom(zoom float64) error {
570 w.mu.RLock()
571 defer w.mu.RUnlock()
572
573 if w.destroyed {
574 return ErrWebViewDestroyed
575 }
576
577 w.view.SetZoomLevel(zoom)
578 return nil
579}
580
581// GetZoom returns the current zoom level
582func (w *WebView) GetZoom() float64 {
583 w.mu.RLock()
584 defer w.mu.RUnlock()
585
586 if w.destroyed {
587 return 1.0
588 }
589
590 return w.view.ZoomLevel()
591}
592
593// UsesDomZoom indicates if this WebView uses DOM-based zoom
594// In gotk4/WebKitGTK, zoom is always viewport-based
595func (w *WebView) UsesDomZoom() bool {
596 return false
597}
598
599// SeedDomZoom is a no-op in gotk4 as we use viewport zoom
600func (w *WebView) SeedDomZoom(zoom float64) error {
601 // Not needed in gotk4 - zoom is handled differently
602 return nil
603}
604
605// InjectScript executes JavaScript in the WebView
606func (w *WebView) InjectScript(script string) error {
607 w.mu.RLock()
608 defer w.mu.RUnlock()
609
610 if w.destroyed {
611 return ErrWebViewDestroyed
612 }
613
614 // Execute JavaScript using our CGO wrapper
615 EvaluateJavascript(w.view, script)
616 return nil
617}
618
619// DispatchCustomEvent dispatches a custom event via JavaScript
620func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error {
621 // Serialize data to JSON
622 jsonData, err := json.Marshal(data)
623 if err != nil {
624 return fmt.Errorf("failed to marshal event data: %w", err)
625 }
626
627 log.Printf("[webkit] Dispatching event '%s' to WebView ID %d with data: %s", eventName, w.id, string(jsonData))
628
629 // Use document.dispatchEvent (not window) so events cross JavaScript world boundaries
630 // The GUI scripts run in an isolated world but share the same Document object
631 script := fmt.Sprintf(`
632 (function() {
633 try {
634 var detail = %s;
635 console.log('[dumber] Dispatching event: %s', detail);
636 document.dispatchEvent(new CustomEvent('%s', { detail: detail }));
637 } catch (e) {
638 console.error('[dumber] Failed to dispatch %s', e);
639 }
640 })();
641 `, string(jsonData), eventName, eventName, eventName)
642 return w.InjectScript(script)
643}
644
645// ShowDevTools opens the WebKit inspector/developer tools
646func (w *WebView) ShowDevTools() error {
647 w.mu.RLock()
648 defer w.mu.RUnlock()
649
650 if w.destroyed {
651 return ErrWebViewDestroyed
652 }
653
654 inspector := w.view.Inspector()
655 if inspector != nil {
656 inspector.Show()
657 }
658 return nil
659}
660
661// ShowPrintDialog shows the print dialog for the current page
662func (w *WebView) ShowPrintDialog() error {
663 w.mu.RLock()
664 defer w.mu.RUnlock()
665
666 if w.destroyed {
667 return ErrWebViewDestroyed
668 }
669
670 // Create and run print operation with GTK print dialog
671 printOp := webkit.NewPrintOperation(w.view)
672 printOp.RunDialog(nil) // nil = no parent window, uses active window
673 return nil
674}
675
676// RegisterKeyboardShortcut registers a keyboard shortcut handler
677// This is a compatibility method - actual shortcut handling is done at the window level
678func (w *WebView) RegisterKeyboardShortcut(key string, modifiers uint, handler func()) error {
679 // TODO: Implement keyboard shortcut registration if needed
680 // For now, shortcuts are handled at the window/application level
681 return nil
682}
683
684// SetWindowFeatures sets window features for popup windows
685func (w *WebView) SetWindowFeatures(features *WindowFeatures) {
686 // This is typically used for popup windows
687 // The features would be applied when creating the window
688}
689
690// IsActive returns whether this WebView is currently active/focused
691func (w *WebView) IsActive() bool {
692 w.mu.RLock()
693 defer w.mu.RUnlock()
694
695 if w.destroyed {
696 return false
697 }
698
699 widget := w.view
700 if widget != nil {
701 return widget.IsFocus()
702 }
703 return false
704}
705
706// Window returns the parent Window of this WebView
707func (w *WebView) Window() *Window {
708 w.mu.RLock()
709 defer w.mu.RUnlock()
710
711 if w.destroyed {
712 return nil
713 }
714
715 return w.window
716}
717
718// UpdateContentFilters updates the content filtering rules
719func (w *WebView) UpdateContentFilters(rules string) error {
720 w.mu.RLock()
721 defer w.mu.RUnlock()
722
723 if w.destroyed {
724 return ErrWebViewDestroyed
725 }
726
727 // Note: Content filtering is handled via InitializeContentBlocking()
728 // This method is kept for backward compatibility but is deprecated
729 log.Printf("[webkit] UpdateContentFilters called but is deprecated - use InitializeContentBlocking instead")
730 return nil
731}
732
733// InitializeContentBlocking initializes content blocking with filter lists
734func (w *WebView) InitializeContentBlocking(filterJSON []byte) error {
735 w.mu.RLock()
736 defer w.mu.RUnlock()
737
738 if w.destroyed {
739 return ErrWebViewDestroyed
740 }
741
742 if len(filterJSON) == 0 {
743 return fmt.Errorf("no filter rules provided")
744 }
745
746 // Apply filters using the content blocking API
747 return ApplyFiltersToWebView(w, filterJSON)
748}
749
750// OnNavigate registers a navigation handler
751func (w *WebView) OnNavigate(handler func(url string)) {
752 w.mu.Lock()
753 defer w.mu.Unlock()
754
755 // This wraps the URI changed handler
756 w.onURIChanged = handler
757}
758
759// RegisterWorkspaceNavigationHandler registers a handler for workspace pane navigation
760// The handler receives the direction ("up", "down", "left", "right") and returns true if handled
761func (w *WebView) RegisterWorkspaceNavigationHandler(handler func(direction string) bool) {
762 w.mu.Lock()
763 defer w.mu.Unlock()
764 w.onWorkspaceNavigation = handler
765}
766
767// RegisterPaneModeHandler registers a handler for pane mode shortcuts
768// The handler receives the action ("enter", "close", "split-right", etc.) and returns true if handled
769func (w *WebView) RegisterPaneModeHandler(handler func(action string) bool, isActiveChecker func() bool) {
770 w.mu.Lock()
771 defer w.mu.Unlock()
772 w.onPaneModeShortcut = handler
773 w.isPaneModeActive = isActiveChecker
774}
775
776// setupFaviconHandlers connects to the FaviconDatabase signals
777func (w *WebView) setupFaviconHandlers() {
778 // Get the NetworkSession from the WebView
779 session := w.view.NetworkSession()
780 if session == nil {
781 log.Printf("[webkit] Warning: No NetworkSession available for favicon handling")
782 return
783 }
784
785 // Get the WebsiteDataManager from the NetworkSession
786 dataManager := session.WebsiteDataManager()
787 if dataManager == nil {
788 log.Printf("[webkit] Warning: No WebsiteDataManager available for favicon handling")
789 return
790 }
791
792 // Enable favicons if not already enabled
793 if !dataManager.FaviconsEnabled() {
794 dataManager.SetFaviconsEnabled(true)
795 log.Printf("[webkit] Enabled favicons for WebView ID %d", w.id)
796 }
797
798 // Get the FaviconDatabase
799 faviconDB := dataManager.FaviconDatabase()
800 if faviconDB == nil {
801 log.Printf("[webkit] Warning: No FaviconDatabase available")
802 return
803 }
804
805 // Connect to the favicon-changed signal
806 faviconDB.ConnectFaviconChanged(func(pageURI, faviconURI string) {
807 log.Printf("[favicon] Favicon changed for %s: %s", pageURI, faviconURI)
808
809 // Call the URI handler if registered
810 w.mu.RLock()
811 handler := w.onFaviconURIChanged
812 w.mu.RUnlock()
813
814 if handler != nil {
815 handler(pageURI, faviconURI)
816 }
817 })
818
819 log.Printf("[webkit] Connected to FaviconDatabase for WebView ID %d", w.id)
820}
821
822// GtkWebView returns the underlying gotk4 WebView for advanced operations
823// This is used when creating related views (popups) that need to share the same session
824func (w *WebView) GtkWebView() *webkit.WebView {
825 w.mu.RLock()
826 defer w.mu.RUnlock()
827 return w.view
828}
829
830// WrapBareWebView creates a minimal WebView wrapper around a bare gotk4 WebView
831// This is used during popup creation to return a WebView to WebKit before initialization
832// Full initialization (container, scripts, event handlers) should be done later
833func WrapBareWebView(bareView *webkit.WebView) *WebView {
834 if bareView == nil {
835 return nil
836 }
837
838 // Generate unique ID
839 id := atomic.AddUint64(&viewIDCounter, 1)
840 log.Printf("[webkit] Created minimal wrapper for bare WebView (ID: %d)", id)
841
842 // Create minimal wrapper - NO initialization yet
843 wv := &WebView{
844 view: bareView,
845 container: nil, // Will be created during full initialization
846 window: nil,
847 id: id,
848 config: nil, // Will be set during full initialization
849 }
850
851 // Register in global registry
852 viewMu.Lock()
853 viewRegistry[id] = wv
854 viewMu.Unlock()
855
856 return wv
857}
858
859// InitializeFromBare completes initialization of a bare WebView wrapper
860// This should be called in ready-to-show after WebKit has configured the view
861func (w *WebView) InitializeFromBare(cfg *Config) error {
862 w.mu.Lock()
863 defer w.mu.Unlock()
864
865 if w.config != nil {
866 return fmt.Errorf("WebView already initialized")
867 }
868
869 if cfg == nil {
870 cfg = GetDefaultConfig()
871 }
872
873 w.config = cfg
874
875 // Verify persistent session
876 viewSession := w.view.NetworkSession()
877 if viewSession == nil {
878 return fmt.Errorf("WebView has no network session")
879 }
880
881 // Setup UserContentManager and inject GUI scripts
882 if err := SetupUserContentManager(w.view, cfg.AppearanceConfigJSON, w.id); err != nil {
883 return fmt.Errorf("failed to setup user content manager: %w", err)
884 }
885
886 // Create container (GtkBox) to hold the WebView
887 container := gtk.NewBox(gtk.OrientationVertical, 0)
888 container.SetHExpand(true)
889 container.SetVExpand(true)
890
891 // Configure WebView widget for expansion
892 w.view.SetHExpand(true)
893 w.view.SetVExpand(true)
894
895 // Add WebView to container
896 container.Append(w.view)
897
898 w.container = container
899
900 // Apply configuration
901 if err := w.applyConfig(); err != nil {
902 return err
903 }
904
905 // Setup event handlers
906 w.setupEventHandlers()
907
908 // Attach keyboard bridge
909 w.AttachKeyboardBridge()
910
911 log.Printf("[webkit] Completed initialization for WebView ID %d", w.id)
912
913 return nil
914}