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