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