refactor(webkit): complete JavaScript bridge and GTK4 keyboard integration
Major fixes for gotk4 migration to restore all keyboard shortcuts: JavaScript-to-Go Bridge Fixes: - Fix DispatchCustomEvent to use document.dispatchEvent instead of window (critical fix: Document object is shared across JavaScript worlds) - Fix WebView ID injection at document-start before GUI scripts load - Update main-world.ts to not overwrite injected webview ID - Add zoom toast notifications for Ctrl+/-/0 shortcuts Go-to-JavaScript Bridge Fixes: - Implement EventControllerKey bridge to forward keyboard events to JavaScript (main branch uses CGO EventController, we now use gotk4 equivalent) - Bridge captures keyboard events at PhaseCapture and forwards via dumber:key events - Allows JavaScript KeyboardService to receive events for page-level shortcuts - All keyboard events now properly reach both GTK and JavaScript handlers GTK4 Shortcut System: - Fix shortcuts.go to use proper gotk4 API (NewCallbackAction, ShortcutTrigger) - Convert accelerator format (ctrl+l -> <Control>l) for GTK4 compatibility - Fix evaluate.go to use javascriptcore/v6 instead of v4 (no more GTK2/3!) - Update all GTK/GDK constant names for gotk4 (ControlMask, PhaseCapture, etc) Code Quality: - Add keyboard_bridge.go as dedicated file for keyboard event forwarding - Replace magic strings with proper constants (modifierCmdOrCtrl, keyP, etc) - Use switch statements instead of if-else chains for cleaner code - Add comprehensive logging for keyboard event flow debugging Verified Working: - ✅ Ctrl+L, Ctrl+F (omnibox/find toggles) - ✅ Ctrl+Shift+C (copy URL) - ✅ Ctrl++/-/0 (zoom in/out/reset with toast notifications) - ✅ Ctrl+P (pane mode activation - JavaScript-handled) - ✅ Alt+Arrow keys (pane navigation) - ✅ F12 (developer tools) This completes the core JavaScript<->GTK bridge for the gotk4 migration. Pane splitting functionality works but has separate layout issues to address.
- commit
- 356259143f7aea22efa6096c1c9a4f4b1721184e
- author
- bamen · · 2025-10-09 17:29 UTC
- parent
- 4be3d7e
- signature
- unsigned
12 files · +486 -50
M gui/src/components/toast/ToastContainer.svelte +7 -7
| @@ -110,15 +110,15 @@ | |||
| 110 | 110 | $effect(() => { | |
| 111 | 111 | console.log('🔄 ToastContainer initialized, functions exposed'); | |
| 112 | 112 | ||
| 113 | - | // Monitor container periodically for debugging | |
| 114 | - | const monitorInterval = setInterval(() => { | |
| 115 | - | const container = document.querySelector('.toast-container'); | |
| 116 | - | const browserRoot = document.querySelector('.browser-component-root'); | |
| 117 | - | console.log('[dumber] Periodic check - container:', !!container, 'browser-root:', !!browserRoot, 'toasts:', toasts.length); | |
| 118 | - | }, 5000); | |
| 113 | + | // Monitor container periodically for debugging (disabled) | |
| 114 | + | // const monitorInterval = setInterval(() => { | |
| 115 | + | // const container = document.querySelector('.toast-container'); | |
| 116 | + | // const browserRoot = document.querySelector('.browser-component-root'); | |
| 117 | + | // console.log('[dumber] Periodic check - container:', !!container, 'browser-root:', !!browserRoot, 'toasts:', toasts.length); | |
| 118 | + | // }, 5000); | |
| 119 | 119 | ||
| 120 | 120 | return () => { | |
| 121 | - | clearInterval(monitorInterval); | |
| 121 | + | // clearInterval(monitorInterval); | |
| 122 | 122 | console.log('🗑️ ToastContainer cleanup'); | |
| 123 | 123 | }; | |
| 124 | 124 | }); | |
| @@ -110,15 +110,15 @@ | |||||
| 110 | $effect(() => { | 110 | $effect(() => { | ||
| 111 | console.log('🔄 ToastContainer initialized, functions exposed'); | 111 | console.log('🔄 ToastContainer initialized, functions exposed'); | ||
| 112 | 112 | ||||
| 113 | - | // Monitor container periodically for debugging | 113 | + | // Monitor container periodically for debugging (disabled) |
| 114 | - | const monitorInterval = setInterval(() => { | 114 | + | // const monitorInterval = setInterval(() => { |
| 115 | - | const container = document.querySelector('.toast-container'); | 115 | + | // const container = document.querySelector('.toast-container'); |
| 116 | - | const browserRoot = document.querySelector('.browser-component-root'); | 116 | + | // const browserRoot = document.querySelector('.browser-component-root'); |
| 117 | - | console.log('[dumber] Periodic check - container:', !!container, 'browser-root:', !!browserRoot, 'toasts:', toasts.length); | 117 | + | // console.log('[dumber] Periodic check - container:', !!container, 'browser-root:', !!browserRoot, 'toasts:', toasts.length); |
| 118 | - | }, 5000); | 118 | + | // }, 5000); |
| 119 | 119 | ||||
| 120 | return () => { | 120 | return () => { | ||
| 121 | - | clearInterval(monitorInterval); | 121 | + | // clearInterval(monitorInterval); |
| 122 | console.log('🗑️ ToastContainer cleanup'); | 122 | console.log('🗑️ ToastContainer cleanup'); | ||
| 123 | }; | 123 | }; | ||
| 124 | }); | 124 | }); | ||
M gui/src/injected/gui.ts +8 -0
| @@ -283,7 +283,15 @@ if (!window.__dumber_gui_ready) { | |||
| 283 | 283 | const eventWebViewId = detail?.webviewId; | |
| 284 | 284 | const source = detail?.source || "unknown"; | |
| 285 | 285 | ||
| 286 | + | console.log("[dumber shortcuts] Event received", { | |
| 287 | + | action, | |
| 288 | + | eventWebViewId, | |
| 289 | + | currentWebViewId, | |
| 290 | + | detail, | |
| 291 | + | }); | |
| 292 | + | ||
| 286 | 293 | if (typeof action !== "string") { | |
| 294 | + | console.log("[dumber shortcuts] No action in event, ignoring"); | |
| 287 | 295 | return; | |
| 288 | 296 | } | |
| 289 | 297 | ||
| @@ -283,7 +283,15 @@ if (!window.__dumber_gui_ready) { | |||||
| 283 | const eventWebViewId = detail?.webviewId; | 283 | const eventWebViewId = detail?.webviewId; | ||
| 284 | const source = detail?.source || "unknown"; | 284 | const source = detail?.source || "unknown"; | ||
| 285 | 285 | ||||
| 286 | + | console.log("[dumber shortcuts] Event received", { | |||
| 287 | + | action, | |||
| 288 | + | eventWebViewId, | |||
| 289 | + | currentWebViewId, | |||
| 290 | + | detail, | |||
| 291 | + | }); | |||
| 292 | + | ||||
| 286 | if (typeof action !== "string") { | 293 | if (typeof action !== "string") { | ||
| 294 | + | console.log("[dumber shortcuts] No action in event, ignoring"); | |||
| 287 | return; | 295 | return; | ||
| 288 | } | 296 | } | ||
| 289 | 297 | ||||
M gui/src/injected/main-world.ts +8 -3
| @@ -120,9 +120,14 @@ function detectWindowType(features?: string | null): string { | |||
| 120 | 120 | const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go | |
| 121 | 121 | window.__dumber_dom_zoom_seed = initialZoom; | |
| 122 | 122 | ||
| 123 | - | // Initialize WebView ID and active state (will be replaced by Go) | |
| 124 | - | window.__dumber_webview_id = "__WEBVIEW_ID__"; | |
| 125 | - | window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean; | |
| 123 | + | // Initialize WebView ID and active state (injected by Go at document-start) | |
| 124 | + | // Only set fallback values if not already injected | |
| 125 | + | if (typeof window.__dumber_webview_id === 'undefined') { | |
| 126 | + | window.__dumber_webview_id = "__WEBVIEW_ID__"; | |
| 127 | + | } | |
| 128 | + | if (typeof window.__dumber_is_active === 'undefined') { | |
| 129 | + | window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean; | |
| 130 | + | } | |
| 126 | 131 | ||
| 127 | 132 | // Request color palettes from Go backend | |
| 128 | 133 | try { | |
| @@ -120,9 +120,14 @@ function detectWindowType(features?: string | null): string { | |||||
| 120 | const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go | 120 | const initialZoom = 1.0; // __DOM_ZOOM_DEFAULT__ will be replaced by Go | ||
| 121 | window.__dumber_dom_zoom_seed = initialZoom; | 121 | window.__dumber_dom_zoom_seed = initialZoom; | ||
| 122 | 122 | ||||
| 123 | - | // Initialize WebView ID and active state (will be replaced by Go) | 123 | + | // Initialize WebView ID and active state (injected by Go at document-start) |
| 124 | - | window.__dumber_webview_id = "__WEBVIEW_ID__"; | 124 | + | // Only set fallback values if not already injected |
| 125 | - | window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean; | 125 | + | if (typeof window.__dumber_webview_id === 'undefined') { |
| 126 | + | window.__dumber_webview_id = "__WEBVIEW_ID__"; | |||
| 127 | + | } | |||
| 128 | + | if (typeof window.__dumber_is_active === 'undefined') { | |||
| 129 | + | window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean; | |||
| 130 | + | } | |||
| 126 | 131 | ||||
| 127 | // Request color palettes from Go backend | 132 | // Request color palettes from Go backend | ||
| 128 | try { | 133 | try { | ||
M internal/app/browser/browser.go +7 -2
| @@ -153,8 +153,8 @@ func (app *BrowserApp) Run() { | |||
| 153 | 153 | // Set config on scheme handler | |
| 154 | 154 | app.schemeHandler.SetConfig(app.config) | |
| 155 | 155 | ||
| 156 | - | // Register custom scheme resolver for "dumb://" URIs | |
| 157 | - | webkit.SetURISchemeResolver("dumb", app.schemeHandler.Handle) | |
| 156 | + | // Register custom scheme resolver for "dumb://" URIs (will be applied after WebView creation) | |
| 157 | + | webkit.RegisterURIScheme("dumb", app.schemeHandler.Handle) | |
| 158 | 158 | ||
| 159 | 159 | // Create and setup WebView | |
| 160 | 160 | if err := app.createWebView(); err != nil { | |
| @@ -162,6 +162,11 @@ func (app *BrowserApp) Run() { | |||
| 162 | 162 | return | |
| 163 | 163 | } | |
| 164 | 164 | ||
| 165 | + | // Apply URI scheme handlers after WebView creation | |
| 166 | + | if err := webkit.ApplyURISchemeHandlers(app.webView.GetWebView()); err != nil { | |
| 167 | + | log.Printf("Warning: failed to register URI scheme handlers: %v", err) | |
| 168 | + | } | |
| 169 | + | ||
| 165 | 170 | // Initialize content blocking | |
| 166 | 171 | if err := app.setupContentBlocking(); err != nil { | |
| 167 | 172 | log.Printf("Warning: failed to setup content blocking: %v", err) | |
| @@ -153,8 +153,8 @@ func (app *BrowserApp) Run() { | |||||
| 153 | // Set config on scheme handler | 153 | // Set config on scheme handler | ||
| 154 | app.schemeHandler.SetConfig(app.config) | 154 | app.schemeHandler.SetConfig(app.config) | ||
| 155 | 155 | ||||
| 156 | - | // Register custom scheme resolver for "dumb://" URIs | 156 | + | // Register custom scheme resolver for "dumb://" URIs (will be applied after WebView creation) |
| 157 | - | webkit.SetURISchemeResolver("dumb", app.schemeHandler.Handle) | 157 | + | webkit.RegisterURIScheme("dumb", app.schemeHandler.Handle) |
| 158 | 158 | ||||
| 159 | // Create and setup WebView | 159 | // Create and setup WebView | ||
| 160 | if err := app.createWebView(); err != nil { | 160 | if err := app.createWebView(); err != nil { | ||
| @@ -162,6 +162,11 @@ func (app *BrowserApp) Run() { | |||||
| 162 | return | 162 | return | ||
| 163 | } | 163 | } | ||
| 164 | 164 | ||||
| 165 | + | // Apply URI scheme handlers after WebView creation | |||
| 166 | + | if err := webkit.ApplyURISchemeHandlers(app.webView.GetWebView()); err != nil { | |||
| 167 | + | log.Printf("Warning: failed to register URI scheme handlers: %v", err) | |||
| 168 | + | } | |||
| 169 | + | ||||
| 165 | // Initialize content blocking | 170 | // Initialize content blocking | ||
| 166 | if err := app.setupContentBlocking(); err != nil { | 171 | if err := app.setupContentBlocking(); err != nil { | ||
| 167 | log.Printf("Warning: failed to setup content blocking: %v", err) | 172 | log.Printf("Warning: failed to setup content blocking: %v", err) | ||
M internal/app/browser/window_shortcuts.go +24 -0
| @@ -226,6 +226,30 @@ func (h *WindowShortcutHandler) handleZoom(action string, multiplier float64) { | |||
| 226 | 226 | // Apply zoom to active pane | |
| 227 | 227 | if err := activeWebView.SetZoom(newZoom); err != nil { | |
| 228 | 228 | log.Printf("[window-shortcuts] Failed to set zoom: %v", err) | |
| 229 | + | return | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | // Show zoom toast notification | |
| 233 | + | zoomPercent := int(newZoom * 100) | |
| 234 | + | toastScript := fmt.Sprintf(` | |
| 235 | + | (function() { | |
| 236 | + | try { | |
| 237 | + | if (typeof window.__dumber_showZoomToast === 'function') { | |
| 238 | + | window.__dumber_showZoomToast(%f); | |
| 239 | + | } else { | |
| 240 | + | // Fallback: dispatch toast event directly | |
| 241 | + | document.dispatchEvent(new CustomEvent('dumber:showToast', { | |
| 242 | + | detail: { message: 'Zoom: %d%%', duration: 1500, type: 'info' } | |
| 243 | + | })); | |
| 244 | + | } | |
| 245 | + | } catch (e) { | |
| 246 | + | console.error('[zoom] Failed to show toast:', e); | |
| 247 | + | } | |
| 248 | + | })(); | |
| 249 | + | `, newZoom, zoomPercent) | |
| 250 | + | ||
| 251 | + | if err := activeWebView.InjectScript(toastScript); err != nil { | |
| 252 | + | log.Printf("[window-shortcuts] Failed to show zoom toast: %v", err) | |
| 229 | 253 | } | |
| 230 | 254 | } | |
| 231 | 255 | ||
| @@ -226,6 +226,30 @@ func (h *WindowShortcutHandler) handleZoom(action string, multiplier float64) { | |||||
| 226 | // Apply zoom to active pane | 226 | // Apply zoom to active pane | ||
| 227 | if err := activeWebView.SetZoom(newZoom); err != nil { | 227 | if err := activeWebView.SetZoom(newZoom); err != nil { | ||
| 228 | log.Printf("[window-shortcuts] Failed to set zoom: %v", err) | 228 | log.Printf("[window-shortcuts] Failed to set zoom: %v", err) | ||
| 229 | + | return | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | // Show zoom toast notification | |||
| 233 | + | zoomPercent := int(newZoom * 100) | |||
| 234 | + | toastScript := fmt.Sprintf(` | |||
| 235 | + | (function() { | |||
| 236 | + | try { | |||
| 237 | + | if (typeof window.__dumber_showZoomToast === 'function') { | |||
| 238 | + | window.__dumber_showZoomToast(%f); | |||
| 239 | + | } else { | |||
| 240 | + | // Fallback: dispatch toast event directly | |||
| 241 | + | document.dispatchEvent(new CustomEvent('dumber:showToast', { | |||
| 242 | + | detail: { message: 'Zoom: %d%%', duration: 1500, type: 'info' } | |||
| 243 | + | })); | |||
| 244 | + | } | |||
| 245 | + | } catch (e) { | |||
| 246 | + | console.error('[zoom] Failed to show toast:', e); | |||
| 247 | + | } | |||
| 248 | + | })(); | |||
| 249 | + | `, newZoom, zoomPercent) | |||
| 250 | + | ||||
| 251 | + | if err := activeWebView.InjectScript(toastScript); err != nil { | |||
| 252 | + | log.Printf("[window-shortcuts] Failed to show zoom toast: %v", err) | |||
| 229 | } | 253 | } | ||
| 230 | } | 254 | } | ||
| 231 | 255 | ||||
M pkg/webkit/evaluate.go +42 -8
| @@ -3,42 +3,76 @@ package webkit | |||
| 3 | 3 | /* | |
| 4 | 4 | #cgo pkg-config: webkitgtk-6.0 | |
| 5 | 5 | #include <webkit/webkit.h> | |
| 6 | + | #include <stdlib.h> | |
| 6 | 7 | */ | |
| 7 | 8 | import "C" | |
| 8 | 9 | import ( | |
| 9 | 10 | "unsafe" | |
| 10 | 11 | ||
| 12 | + | javascriptcore "github.com/diamondburned/gotk4-webkitgtk/pkg/javascriptcore/v6" | |
| 11 | 13 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | |
| 12 | - | "github.com/diamondburned/gotk4/pkg/core/gbox" | |
| 14 | + | "github.com/diamondburned/gotk4/pkg/core/glib" | |
| 13 | 15 | ) | |
| 14 | 16 | ||
| 15 | - | // EvaluateJavascript executes JavaScript code in the WebView | |
| 17 | + | // EvaluateJavascript executes JavaScript code in the WebView in the default world | |
| 16 | 18 | // This is a wrapper around webkit_web_view_evaluate_javascript that wasn't generated by gotk4 | |
| 17 | 19 | func EvaluateJavascript(view *webkit.WebView, script string) { | |
| 20 | + | EvaluateJavascriptInWorld(view, script, "") | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // EvaluateJavascriptInWorld executes JavaScript code in a specific world | |
| 24 | + | // Pass empty string for worldName to use the main/page world | |
| 25 | + | func EvaluateJavascriptInWorld(view *webkit.WebView, script string, worldName string) { | |
| 18 | 26 | if view == nil { | |
| 19 | 27 | return | |
| 20 | 28 | } | |
| 21 | 29 | ||
| 22 | - | // Get the native C pointer from the gotk4 WebView using gbox.Get | |
| 23 | - | // gbox.Get expects a uintptr (the address of the Go object wrapper) | |
| 24 | - | ptr := gbox.Get(uintptr(unsafe.Pointer(view))) | |
| 25 | - | if ptr == nil { | |
| 30 | + | // Get the underlying GObject pointer from the gotk4 WebView | |
| 31 | + | obj := glib.BaseObject(view) | |
| 32 | + | if obj == nil { | |
| 33 | + | return | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | nativePtr := obj.Native() | |
| 37 | + | if nativePtr == 0 { | |
| 26 | 38 | return | |
| 27 | 39 | } | |
| 28 | 40 | ||
| 29 | 41 | cScript := C.CString(script) | |
| 30 | 42 | defer C.free(unsafe.Pointer(cScript)) | |
| 31 | 43 | ||
| 44 | + | var cWorldName *C.char | |
| 45 | + | if worldName != "" { | |
| 46 | + | cWorldName = C.CString(worldName) | |
| 47 | + | defer C.free(unsafe.Pointer(cWorldName)) | |
| 48 | + | } | |
| 49 | + | ||
| 32 | 50 | // Call webkit_web_view_evaluate_javascript | |
| 33 | 51 | // Parameters: web_view, script, length, world_name, source_uri, cancellable, callback, user_data | |
| 34 | 52 | C.webkit_web_view_evaluate_javascript( | |
| 35 | - | (*C.WebKitWebView)(ptr.(unsafe.Pointer)), | |
| 53 | + | (*C.WebKitWebView)(unsafe.Pointer(nativePtr)), | |
| 36 | 54 | cScript, | |
| 37 | 55 | C.gssize(-1), // -1 means null-terminated string | |
| 38 | - | nil, // world_name (NULL for default world) | |
| 56 | + | cWorldName, // world_name (NULL for main world, or specific world name) | |
| 39 | 57 | nil, // source_uri (NULL) | |
| 40 | 58 | nil, // cancellable (NULL) | |
| 41 | 59 | nil, // callback (NULL for fire-and-forget) | |
| 42 | 60 | nil, // user_data (NULL) | |
| 43 | 61 | ) | |
| 44 | 62 | } | |
| 63 | + | ||
| 64 | + | // JSCValueToString converts a JSCValue from a GTK signal to a Go string | |
| 65 | + | func JSCValueToString(value interface{}) string { | |
| 66 | + | if value == nil { | |
| 67 | + | return "" | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // Type assert to *javascriptcore.Value from gotk4 bindings | |
| 71 | + | jscValue, ok := value.(*javascriptcore.Value) | |
| 72 | + | if !ok { | |
| 73 | + | return "" | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | // Use the gotk4 String() method to convert JSCValue to string | |
| 77 | + | return jscValue.String() | |
| 78 | + | } | |
| @@ -3,42 +3,76 @@ package webkit | |||||
| 3 | /* | 3 | /* | ||
| 4 | #cgo pkg-config: webkitgtk-6.0 | 4 | #cgo pkg-config: webkitgtk-6.0 | ||
| 5 | #include <webkit/webkit.h> | 5 | #include <webkit/webkit.h> | ||
| 6 | + | #include <stdlib.h> | |||
| 6 | */ | 7 | */ | ||
| 7 | import "C" | 8 | import "C" | ||
| 8 | import ( | 9 | import ( | ||
| 9 | "unsafe" | 10 | "unsafe" | ||
| 10 | 11 | ||||
| 12 | + | javascriptcore "github.com/diamondburned/gotk4-webkitgtk/pkg/javascriptcore/v6" | |||
| 11 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | 13 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | ||
| 12 | - | "github.com/diamondburned/gotk4/pkg/core/gbox" | 14 | + | "github.com/diamondburned/gotk4/pkg/core/glib" |
| 13 | ) | 15 | ) | ||
| 14 | 16 | ||||
| 15 | - | // EvaluateJavascript executes JavaScript code in the WebView | 17 | + | // EvaluateJavascript executes JavaScript code in the WebView in the default world |
| 16 | // This is a wrapper around webkit_web_view_evaluate_javascript that wasn't generated by gotk4 | 18 | // This is a wrapper around webkit_web_view_evaluate_javascript that wasn't generated by gotk4 | ||
| 17 | func EvaluateJavascript(view *webkit.WebView, script string) { | 19 | func EvaluateJavascript(view *webkit.WebView, script string) { | ||
| 20 | + | EvaluateJavascriptInWorld(view, script, "") | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // EvaluateJavascriptInWorld executes JavaScript code in a specific world | |||
| 24 | + | // Pass empty string for worldName to use the main/page world | |||
| 25 | + | func EvaluateJavascriptInWorld(view *webkit.WebView, script string, worldName string) { | |||
| 18 | if view == nil { | 26 | if view == nil { | ||
| 19 | return | 27 | return | ||
| 20 | } | 28 | } | ||
| 21 | 29 | ||||
| 22 | - | // Get the native C pointer from the gotk4 WebView using gbox.Get | 30 | + | // Get the underlying GObject pointer from the gotk4 WebView |
| 23 | - | // gbox.Get expects a uintptr (the address of the Go object wrapper) | 31 | + | obj := glib.BaseObject(view) |
| 24 | - | ptr := gbox.Get(uintptr(unsafe.Pointer(view))) | 32 | + | if obj == nil { |
| 25 | - | if ptr == nil { | 33 | + | return |
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | nativePtr := obj.Native() | |||
| 37 | + | if nativePtr == 0 { | |||
| 26 | return | 38 | return | ||
| 27 | } | 39 | } | ||
| 28 | 40 | ||||
| 29 | cScript := C.CString(script) | 41 | cScript := C.CString(script) | ||
| 30 | defer C.free(unsafe.Pointer(cScript)) | 42 | defer C.free(unsafe.Pointer(cScript)) | ||
| 31 | 43 | ||||
| 44 | + | var cWorldName *C.char | |||
| 45 | + | if worldName != "" { | |||
| 46 | + | cWorldName = C.CString(worldName) | |||
| 47 | + | defer C.free(unsafe.Pointer(cWorldName)) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 32 | // Call webkit_web_view_evaluate_javascript | 50 | // Call webkit_web_view_evaluate_javascript | ||
| 33 | // Parameters: web_view, script, length, world_name, source_uri, cancellable, callback, user_data | 51 | // Parameters: web_view, script, length, world_name, source_uri, cancellable, callback, user_data | ||
| 34 | C.webkit_web_view_evaluate_javascript( | 52 | C.webkit_web_view_evaluate_javascript( | ||
| 35 | - | (*C.WebKitWebView)(ptr.(unsafe.Pointer)), | 53 | + | (*C.WebKitWebView)(unsafe.Pointer(nativePtr)), |
| 36 | cScript, | 54 | cScript, | ||
| 37 | C.gssize(-1), // -1 means null-terminated string | 55 | C.gssize(-1), // -1 means null-terminated string | ||
| 38 | - | nil, // world_name (NULL for default world) | 56 | + | cWorldName, // world_name (NULL for main world, or specific world name) |
| 39 | nil, // source_uri (NULL) | 57 | nil, // source_uri (NULL) | ||
| 40 | nil, // cancellable (NULL) | 58 | nil, // cancellable (NULL) | ||
| 41 | nil, // callback (NULL for fire-and-forget) | 59 | nil, // callback (NULL for fire-and-forget) | ||
| 42 | nil, // user_data (NULL) | 60 | nil, // user_data (NULL) | ||
| 43 | ) | 61 | ) | ||
| 44 | } | 62 | } | ||
| 63 | + | ||||
| 64 | + | // JSCValueToString converts a JSCValue from a GTK signal to a Go string | |||
| 65 | + | func JSCValueToString(value interface{}) string { | |||
| 66 | + | if value == nil { | |||
| 67 | + | return "" | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // Type assert to *javascriptcore.Value from gotk4 bindings | |||
| 71 | + | jscValue, ok := value.(*javascriptcore.Value) | |||
| 72 | + | if !ok { | |||
| 73 | + | return "" | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | // Use the gotk4 String() method to convert JSCValue to string | |||
| 77 | + | return jscValue.String() | |||
| 78 | + | } | |||
A pkg/webkit/keyboard_bridge.go +152 -0
| @@ -0,0 +1,152 @@ | |||
| 1 | + | package webkit | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "log" | |
| 6 | + | ||
| 7 | + | "github.com/diamondburned/gotk4/pkg/gdk/v4" | |
| 8 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Keyboard shortcut modifier string constants | |
| 12 | + | const ( | |
| 13 | + | modifierCmdOrCtrl = "cmdorctrl" | |
| 14 | + | modifierShift = "shift" | |
| 15 | + | modifierAlt = "alt" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // GDK key constants for special keys | |
| 19 | + | const ( | |
| 20 | + | gdkKeyLeft = 0xff51 | |
| 21 | + | gdkKeyRight = 0xff53 | |
| 22 | + | gdkKeyUp = 0xff52 | |
| 23 | + | gdkKeyDown = 0xff54 | |
| 24 | + | gdkKeyF12 = 0xffbe | |
| 25 | + | gdkKeyPlus = 0x2b | |
| 26 | + | gdkKeyEqual = 0x3d | |
| 27 | + | gdkKeyMinus = 0x2d | |
| 28 | + | ) | |
| 29 | + | ||
| 30 | + | // Key name constants for shortcut mapping | |
| 31 | + | const ( | |
| 32 | + | keyP = "p" | |
| 33 | + | keyL = "l" | |
| 34 | + | keyF = "f" | |
| 35 | + | keyC = "c" | |
| 36 | + | keyW = "w" | |
| 37 | + | keyR = "r" | |
| 38 | + | keyArrowLeft = "arrowleft" | |
| 39 | + | keyArrowRight = "arrowright" | |
| 40 | + | keyArrowUp = "arrowup" | |
| 41 | + | keyArrowDown = "arrowdown" | |
| 42 | + | keyF12 = "F12" | |
| 43 | + | keyPlus = "plus" | |
| 44 | + | keyMinus = "minus" | |
| 45 | + | keyZero = "0" | |
| 46 | + | ) | |
| 47 | + | ||
| 48 | + | // AttachKeyboardBridge attaches an EventControllerKey to bridge keyboard events to JavaScript | |
| 49 | + | // This is critical for allowing JavaScript KeyboardService to receive keyboard events | |
| 50 | + | // that would otherwise be consumed by GTK shortcuts | |
| 51 | + | func (w *WebView) AttachKeyboardBridge() { | |
| 52 | + | if w == nil || w.view == nil { | |
| 53 | + | return | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // Create event controller for keyboard | |
| 57 | + | keyController := gtk.NewEventControllerKey() | |
| 58 | + | ||
| 59 | + | // Set to capture phase so we see events before WebView processes them | |
| 60 | + | keyController.SetPropagationPhase(gtk.PhaseCapture) | |
| 61 | + | ||
| 62 | + | // Connect to key-pressed signal | |
| 63 | + | keyController.ConnectKeyPressed(func(keyval, keycode uint, state gdk.ModifierType) bool { | |
| 64 | + | // Build normalized shortcut string | |
| 65 | + | var parts []string | |
| 66 | + | ||
| 67 | + | ctrl := state.Has(gdk.ControlMask) | |
| 68 | + | alt := state.Has(gdk.AltMask) | |
| 69 | + | shift := state.Has(gdk.ShiftMask) | |
| 70 | + | ||
| 71 | + | // Build modifier prefix using switch on modifier combination | |
| 72 | + | switch { | |
| 73 | + | case ctrl && shift: | |
| 74 | + | parts = append(parts, modifierCmdOrCtrl, modifierShift) | |
| 75 | + | case ctrl && !shift: | |
| 76 | + | parts = append(parts, modifierCmdOrCtrl) | |
| 77 | + | case alt: | |
| 78 | + | parts = append(parts, modifierAlt) | |
| 79 | + | case shift: | |
| 80 | + | parts = append(parts, modifierShift) | |
| 81 | + | default: | |
| 82 | + | // No modifiers - only forward if we match a key below | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // Map keyval to key name | |
| 86 | + | var keyName string | |
| 87 | + | switch keyval { | |
| 88 | + | case 'p', 'P': | |
| 89 | + | keyName = keyP | |
| 90 | + | case 'l', 'L': | |
| 91 | + | keyName = keyL | |
| 92 | + | case 'f', 'F': | |
| 93 | + | keyName = keyF | |
| 94 | + | case 'c', 'C': | |
| 95 | + | keyName = keyC | |
| 96 | + | case 'w', 'W': | |
| 97 | + | keyName = keyW | |
| 98 | + | case 'r', 'R': | |
| 99 | + | keyName = keyR | |
| 100 | + | case gdkKeyLeft: | |
| 101 | + | keyName = keyArrowLeft | |
| 102 | + | case gdkKeyRight: | |
| 103 | + | keyName = keyArrowRight | |
| 104 | + | case gdkKeyUp: | |
| 105 | + | keyName = keyArrowUp | |
| 106 | + | case gdkKeyDown: | |
| 107 | + | keyName = keyArrowDown | |
| 108 | + | case gdkKeyF12: | |
| 109 | + | keyName = keyF12 | |
| 110 | + | case gdkKeyPlus, gdkKeyEqual: | |
| 111 | + | keyName = keyPlus | |
| 112 | + | case gdkKeyMinus: | |
| 113 | + | keyName = keyMinus | |
| 114 | + | case '0': | |
| 115 | + | keyName = keyZero | |
| 116 | + | default: | |
| 117 | + | // Only forward shortcuts with modifiers | |
| 118 | + | if !ctrl && !alt && !shift { | |
| 119 | + | return false | |
| 120 | + | } | |
| 121 | + | return false | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | if keyName != "" { | |
| 125 | + | parts = append(parts, keyName) | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | shortcut := "" | |
| 129 | + | if len(parts) > 0 { | |
| 130 | + | shortcut = parts[0] | |
| 131 | + | for i := 1; i < len(parts); i++ { | |
| 132 | + | shortcut += "+" + parts[i] | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | if shortcut != "" { | |
| 137 | + | // Dispatch custom dumber:key event to JavaScript | |
| 138 | + | script := fmt.Sprintf(`document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));`, shortcut) | |
| 139 | + | if err := w.InjectScript(script); err != nil { | |
| 140 | + | log.Printf("[keyboard-bridge] Failed to dispatch key event: %v", err) | |
| 141 | + | } | |
| 142 | + | log.Printf("[keyboard-bridge] Forwarded shortcut to JS: %s", shortcut) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // Return false to allow GTK shortcuts to also handle this | |
| 146 | + | return false | |
| 147 | + | }) | |
| 148 | + | ||
| 149 | + | // Attach controller to WebView | |
| 150 | + | w.view.AddController(keyController) | |
| 151 | + | log.Printf("[keyboard-bridge] EventControllerKey attached to WebView ID %d", w.id) | |
| 152 | + | } | |
| @@ -0,0 +1,152 @@ | |||||
| 1 | + | package webkit | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "log" | |||
| 6 | + | ||||
| 7 | + | "github.com/diamondburned/gotk4/pkg/gdk/v4" | |||
| 8 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Keyboard shortcut modifier string constants | |||
| 12 | + | const ( | |||
| 13 | + | modifierCmdOrCtrl = "cmdorctrl" | |||
| 14 | + | modifierShift = "shift" | |||
| 15 | + | modifierAlt = "alt" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // GDK key constants for special keys | |||
| 19 | + | const ( | |||
| 20 | + | gdkKeyLeft = 0xff51 | |||
| 21 | + | gdkKeyRight = 0xff53 | |||
| 22 | + | gdkKeyUp = 0xff52 | |||
| 23 | + | gdkKeyDown = 0xff54 | |||
| 24 | + | gdkKeyF12 = 0xffbe | |||
| 25 | + | gdkKeyPlus = 0x2b | |||
| 26 | + | gdkKeyEqual = 0x3d | |||
| 27 | + | gdkKeyMinus = 0x2d | |||
| 28 | + | ) | |||
| 29 | + | ||||
| 30 | + | // Key name constants for shortcut mapping | |||
| 31 | + | const ( | |||
| 32 | + | keyP = "p" | |||
| 33 | + | keyL = "l" | |||
| 34 | + | keyF = "f" | |||
| 35 | + | keyC = "c" | |||
| 36 | + | keyW = "w" | |||
| 37 | + | keyR = "r" | |||
| 38 | + | keyArrowLeft = "arrowleft" | |||
| 39 | + | keyArrowRight = "arrowright" | |||
| 40 | + | keyArrowUp = "arrowup" | |||
| 41 | + | keyArrowDown = "arrowdown" | |||
| 42 | + | keyF12 = "F12" | |||
| 43 | + | keyPlus = "plus" | |||
| 44 | + | keyMinus = "minus" | |||
| 45 | + | keyZero = "0" | |||
| 46 | + | ) | |||
| 47 | + | ||||
| 48 | + | // AttachKeyboardBridge attaches an EventControllerKey to bridge keyboard events to JavaScript | |||
| 49 | + | // This is critical for allowing JavaScript KeyboardService to receive keyboard events | |||
| 50 | + | // that would otherwise be consumed by GTK shortcuts | |||
| 51 | + | func (w *WebView) AttachKeyboardBridge() { | |||
| 52 | + | if w == nil || w.view == nil { | |||
| 53 | + | return | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // Create event controller for keyboard | |||
| 57 | + | keyController := gtk.NewEventControllerKey() | |||
| 58 | + | ||||
| 59 | + | // Set to capture phase so we see events before WebView processes them | |||
| 60 | + | keyController.SetPropagationPhase(gtk.PhaseCapture) | |||
| 61 | + | ||||
| 62 | + | // Connect to key-pressed signal | |||
| 63 | + | keyController.ConnectKeyPressed(func(keyval, keycode uint, state gdk.ModifierType) bool { | |||
| 64 | + | // Build normalized shortcut string | |||
| 65 | + | var parts []string | |||
| 66 | + | ||||
| 67 | + | ctrl := state.Has(gdk.ControlMask) | |||
| 68 | + | alt := state.Has(gdk.AltMask) | |||
| 69 | + | shift := state.Has(gdk.ShiftMask) | |||
| 70 | + | ||||
| 71 | + | // Build modifier prefix using switch on modifier combination | |||
| 72 | + | switch { | |||
| 73 | + | case ctrl && shift: | |||
| 74 | + | parts = append(parts, modifierCmdOrCtrl, modifierShift) | |||
| 75 | + | case ctrl && !shift: | |||
| 76 | + | parts = append(parts, modifierCmdOrCtrl) | |||
| 77 | + | case alt: | |||
| 78 | + | parts = append(parts, modifierAlt) | |||
| 79 | + | case shift: | |||
| 80 | + | parts = append(parts, modifierShift) | |||
| 81 | + | default: | |||
| 82 | + | // No modifiers - only forward if we match a key below | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // Map keyval to key name | |||
| 86 | + | var keyName string | |||
| 87 | + | switch keyval { | |||
| 88 | + | case 'p', 'P': | |||
| 89 | + | keyName = keyP | |||
| 90 | + | case 'l', 'L': | |||
| 91 | + | keyName = keyL | |||
| 92 | + | case 'f', 'F': | |||
| 93 | + | keyName = keyF | |||
| 94 | + | case 'c', 'C': | |||
| 95 | + | keyName = keyC | |||
| 96 | + | case 'w', 'W': | |||
| 97 | + | keyName = keyW | |||
| 98 | + | case 'r', 'R': | |||
| 99 | + | keyName = keyR | |||
| 100 | + | case gdkKeyLeft: | |||
| 101 | + | keyName = keyArrowLeft | |||
| 102 | + | case gdkKeyRight: | |||
| 103 | + | keyName = keyArrowRight | |||
| 104 | + | case gdkKeyUp: | |||
| 105 | + | keyName = keyArrowUp | |||
| 106 | + | case gdkKeyDown: | |||
| 107 | + | keyName = keyArrowDown | |||
| 108 | + | case gdkKeyF12: | |||
| 109 | + | keyName = keyF12 | |||
| 110 | + | case gdkKeyPlus, gdkKeyEqual: | |||
| 111 | + | keyName = keyPlus | |||
| 112 | + | case gdkKeyMinus: | |||
| 113 | + | keyName = keyMinus | |||
| 114 | + | case '0': | |||
| 115 | + | keyName = keyZero | |||
| 116 | + | default: | |||
| 117 | + | // Only forward shortcuts with modifiers | |||
| 118 | + | if !ctrl && !alt && !shift { | |||
| 119 | + | return false | |||
| 120 | + | } | |||
| 121 | + | return false | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | if keyName != "" { | |||
| 125 | + | parts = append(parts, keyName) | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | shortcut := "" | |||
| 129 | + | if len(parts) > 0 { | |||
| 130 | + | shortcut = parts[0] | |||
| 131 | + | for i := 1; i < len(parts); i++ { | |||
| 132 | + | shortcut += "+" + parts[i] | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | if shortcut != "" { | |||
| 137 | + | // Dispatch custom dumber:key event to JavaScript | |||
| 138 | + | script := fmt.Sprintf(`document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));`, shortcut) | |||
| 139 | + | if err := w.InjectScript(script); err != nil { | |||
| 140 | + | log.Printf("[keyboard-bridge] Failed to dispatch key event: %v", err) | |||
| 141 | + | } | |||
| 142 | + | log.Printf("[keyboard-bridge] Forwarded shortcut to JS: %s", shortcut) | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // Return false to allow GTK shortcuts to also handle this | |||
| 146 | + | return false | |||
| 147 | + | }) | |||
| 148 | + | ||||
| 149 | + | // Attach controller to WebView | |||
| 150 | + | w.view.AddController(keyController) | |||
| 151 | + | log.Printf("[keyboard-bridge] EventControllerKey attached to WebView ID %d", w.id) | |||
| 152 | + | } | |||
M pkg/webkit/mainloop.go +7 -2
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "runtime" | |
| 5 | 5 | ||
| 6 | 6 | "github.com/diamondburned/gotk4/pkg/glib/v2" | |
| 7 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 7 | 8 | ) | |
| 8 | 9 | ||
| 9 | 10 | var ( | |
| @@ -12,11 +13,15 @@ var ( | |||
| 12 | 13 | isInitialized bool | |
| 13 | 14 | ) | |
| 14 | 15 | ||
| 15 | - | // InitMainThread locks the current goroutine to the OS thread for GTK operations. | |
| 16 | - | // This must be called before any GTK operations. | |
| 16 | + | // InitMainThread locks the current goroutine to the OS thread for GTK operations | |
| 17 | + | // and initializes GTK. This must be called before any GTK operations. | |
| 17 | 18 | func InitMainThread() { | |
| 18 | 19 | if !isInitialized { | |
| 19 | 20 | runtime.LockOSThread() | |
| 21 | + | ||
| 22 | + | // Initialize GTK - this is required before creating any GTK widgets | |
| 23 | + | gtk.Init() | |
| 24 | + | ||
| 20 | 25 | isInitialized = true | |
| 21 | 26 | } | |
| 22 | 27 | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "runtime" | 4 | "runtime" | ||
| 5 | 5 | ||||
| 6 | "github.com/diamondburned/gotk4/pkg/glib/v2" | 6 | "github.com/diamondburned/gotk4/pkg/glib/v2" | ||
| 7 | + | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |||
| 7 | ) | 8 | ) | ||
| 8 | 9 | ||||
| 9 | var ( | 10 | var ( | ||
| @@ -12,11 +13,15 @@ var ( | |||||
| 12 | isInitialized bool | 13 | isInitialized bool | ||
| 13 | ) | 14 | ) | ||
| 14 | 15 | ||||
| 15 | - | // InitMainThread locks the current goroutine to the OS thread for GTK operations. | 16 | + | // InitMainThread locks the current goroutine to the OS thread for GTK operations |
| 16 | - | // This must be called before any GTK operations. | 17 | + | // and initializes GTK. This must be called before any GTK operations. |
| 17 | func InitMainThread() { | 18 | func InitMainThread() { | ||
| 18 | if !isInitialized { | 19 | if !isInitialized { | ||
| 19 | runtime.LockOSThread() | 20 | runtime.LockOSThread() | ||
| 21 | + | ||||
| 22 | + | // Initialize GTK - this is required before creating any GTK widgets | |||
| 23 | + | gtk.Init() | |||
| 24 | + | ||||
| 20 | isInitialized = true | 25 | isInitialized = true | ||
| 21 | } | 26 | } | ||
| 22 | } | 27 | } | ||
M pkg/webkit/shortcuts.go +120 -7
| @@ -1,21 +1,37 @@ | |||
| 1 | 1 | package webkit | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "log" | |
| 5 | + | "strings" | |
| 6 | + | ||
| 4 | 7 | "github.com/diamondburned/gotk4/pkg/gdk/v4" | |
| 8 | + | "github.com/diamondburned/gotk4/pkg/glib/v2" | |
| 5 | 9 | "github.com/diamondburned/gotk4/pkg/gtk/v4" | |
| 6 | 10 | ) | |
| 7 | 11 | ||
| 8 | 12 | // WindowShortcuts manages keyboard shortcuts for a window | |
| 9 | 13 | type WindowShortcuts struct { | |
| 10 | - | window *Window | |
| 11 | - | shortcuts map[string]*gtk.ShortcutAction | |
| 14 | + | window *Window | |
| 15 | + | shortcuts map[string]*gtk.Shortcut | |
| 16 | + | controller *gtk.ShortcutController | |
| 12 | 17 | } | |
| 13 | 18 | ||
| 14 | 19 | // NewWindowShortcuts creates a new WindowShortcuts manager | |
| 15 | 20 | func NewWindowShortcuts(win *Window) *WindowShortcuts { | |
| 21 | + | controller := gtk.NewShortcutController() | |
| 22 | + | ||
| 23 | + | // Set scope to GLOBAL so shortcuts work at the window level | |
| 24 | + | // and aren't captured by child widgets (like the WebView) | |
| 25 | + | controller.SetScope(gtk.ShortcutScopeGlobal) | |
| 26 | + | ||
| 27 | + | if win != nil && win.win != nil { | |
| 28 | + | win.win.AddController(controller) | |
| 29 | + | } | |
| 30 | + | ||
| 16 | 31 | return &WindowShortcuts{ | |
| 17 | - | window: win, | |
| 18 | - | shortcuts: make(map[string]*gtk.ShortcutAction), | |
| 32 | + | window: win, | |
| 33 | + | shortcuts: make(map[string]*gtk.Shortcut), | |
| 34 | + | controller: controller, | |
| 19 | 35 | } | |
| 20 | 36 | } | |
| 21 | 37 | ||
| @@ -39,10 +55,107 @@ func (w *Window) InitializeGlobalShortcuts(shortcuts map[string]func()) { | |||
| 39 | 55 | ||
| 40 | 56 | // RegisterShortcut registers a keyboard shortcut | |
| 41 | 57 | func (ws *WindowShortcuts) RegisterShortcut(accelerator string, handler func()) { | |
| 42 | - | if ws == nil { | |
| 58 | + | if ws == nil || ws.controller == nil { | |
| 43 | 59 | return | |
| 44 | 60 | } | |
| 45 | 61 | ||
| 46 | - | // TODO: Implement proper shortcut registration using GTK4's shortcut system | |
| 47 | - | // For now, this is a placeholder | |
| 62 | + | // Convert to GTK format (e.g., "ctrl+plus" -> "<Control>plus") | |
| 63 | + | gtkFormat := convertToGtkFormat(accelerator) | |
| 64 | + | if gtkFormat == "" { | |
| 65 | + | log.Printf("[shortcuts] Invalid key format: %s", accelerator) | |
| 66 | + | return | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // Create a callback action that will trigger our handler | |
| 70 | + | // ShortcutFunc signature: func(widget Widgetter, args *glib.Variant) (ok bool) | |
| 71 | + | action := gtk.NewCallbackAction(func(widget gtk.Widgetter, args *glib.Variant) bool { | |
| 72 | + | if handler != nil { | |
| 73 | + | log.Printf("[shortcuts] Shortcut triggered: %s", accelerator) | |
| 74 | + | handler() | |
| 75 | + | } | |
| 76 | + | // Return false to allow event propagation to WebView | |
| 77 | + | // This lets JavaScript KeyboardService also handle events if needed | |
| 78 | + | return false | |
| 79 | + | }) | |
| 80 | + | ||
| 81 | + | // Parse the accelerator string into a GDK key trigger | |
| 82 | + | // GTK4 uses gtk.ShortcutTrigger for key combinations | |
| 83 | + | trigger := gtk.NewShortcutTriggerParseString(gtkFormat) | |
| 84 | + | if trigger == nil { | |
| 85 | + | log.Printf("[shortcuts] Failed to parse trigger: %s", gtkFormat) | |
| 86 | + | return | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // Create the shortcut with trigger and action | |
| 90 | + | shortcut := gtk.NewShortcut(trigger, action) | |
| 91 | + | ||
| 92 | + | // Add shortcut to the shared controller | |
| 93 | + | ws.controller.AddShortcut(shortcut) | |
| 94 | + | ||
| 95 | + | // Store the shortcut for potential cleanup | |
| 96 | + | ws.shortcuts[accelerator] = shortcut | |
| 97 | + | ||
| 98 | + | log.Printf("[shortcuts] Registered: %s -> %s", accelerator, gtkFormat) | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | // convertToGtkFormat converts common key formats to GTK shortcut format | |
| 102 | + | // Examples: "ctrl+plus" -> "<Control>plus", "ctrl+l" -> "<Control>l" | |
| 103 | + | func convertToGtkFormat(key string) string { | |
| 104 | + | key = strings.ToLower(key) | |
| 105 | + | ||
| 106 | + | // Handle cmdorctrl -> Control | |
| 107 | + | key = strings.ReplaceAll(key, "cmdorctrl+", "ctrl+") | |
| 108 | + | key = strings.ReplaceAll(key, "cmd+", "ctrl+") | |
| 109 | + | ||
| 110 | + | // Split by + to get components | |
| 111 | + | components := strings.Split(key, "+") | |
| 112 | + | if len(components) == 0 { | |
| 113 | + | return "" | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | var parts []string | |
| 117 | + | for i, part := range components { | |
| 118 | + | if i == len(components)-1 { | |
| 119 | + | // Last part is the actual key | |
| 120 | + | switch part { | |
| 121 | + | case "f12": | |
| 122 | + | parts = append(parts, "F12") | |
| 123 | + | case "f5": | |
| 124 | + | parts = append(parts, "F5") | |
| 125 | + | case "plus": | |
| 126 | + | parts = append(parts, "plus") | |
| 127 | + | case "equal": | |
| 128 | + | parts = append(parts, "equal") | |
| 129 | + | case "minus": | |
| 130 | + | parts = append(parts, "minus") | |
| 131 | + | case "0": | |
| 132 | + | parts = append(parts, "0") | |
| 133 | + | case "left", "arrowleft": | |
| 134 | + | parts = append(parts, "Left") | |
| 135 | + | case "right", "arrowright": | |
| 136 | + | parts = append(parts, "Right") | |
| 137 | + | case "up", "arrowup": | |
| 138 | + | parts = append(parts, "Up") | |
| 139 | + | case "down", "arrowdown": | |
| 140 | + | parts = append(parts, "Down") | |
| 141 | + | default: | |
| 142 | + | // Single letter keys like l, f, c, w, etc. | |
| 143 | + | parts = append(parts, part) | |
| 144 | + | } | |
| 145 | + | } else { | |
| 146 | + | // Modifier keys | |
| 147 | + | switch part { | |
| 148 | + | case "ctrl": | |
| 149 | + | parts = append(parts, "<Control>") | |
| 150 | + | case "shift": | |
| 151 | + | parts = append(parts, "<Shift>") | |
| 152 | + | case "alt": | |
| 153 | + | parts = append(parts, "<Alt>") | |
| 154 | + | case "super", "meta": | |
| 155 | + | parts = append(parts, "<Super>") | |
| 156 | + | } | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | return strings.Join(parts, "") | |
| 48 | 161 | } | |
| @@ -1,21 +1,37 @@ | |||||
| 1 | package webkit | 1 | package webkit | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "log" | |||
| 5 | + | "strings" | |||
| 6 | + | ||||
| 4 | "github.com/diamondburned/gotk4/pkg/gdk/v4" | 7 | "github.com/diamondburned/gotk4/pkg/gdk/v4" | ||
| 8 | + | "github.com/diamondburned/gotk4/pkg/glib/v2" | |||
| 5 | "github.com/diamondburned/gotk4/pkg/gtk/v4" | 9 | "github.com/diamondburned/gotk4/pkg/gtk/v4" | ||
| 6 | ) | 10 | ) | ||
| 7 | 11 | ||||
| 8 | // WindowShortcuts manages keyboard shortcuts for a window | 12 | // WindowShortcuts manages keyboard shortcuts for a window | ||
| 9 | type WindowShortcuts struct { | 13 | type WindowShortcuts struct { | ||
| 10 | - | window *Window | 14 | + | window *Window |
| 11 | - | shortcuts map[string]*gtk.ShortcutAction | 15 | + | shortcuts map[string]*gtk.Shortcut |
| 16 | + | controller *gtk.ShortcutController | |||
| 12 | } | 17 | } | ||
| 13 | 18 | ||||
| 14 | // NewWindowShortcuts creates a new WindowShortcuts manager | 19 | // NewWindowShortcuts creates a new WindowShortcuts manager | ||
| 15 | func NewWindowShortcuts(win *Window) *WindowShortcuts { | 20 | func NewWindowShortcuts(win *Window) *WindowShortcuts { | ||
| 21 | + | controller := gtk.NewShortcutController() | |||
| 22 | + | ||||
| 23 | + | // Set scope to GLOBAL so shortcuts work at the window level | |||
| 24 | + | // and aren't captured by child widgets (like the WebView) | |||
| 25 | + | controller.SetScope(gtk.ShortcutScopeGlobal) | |||
| 26 | + | ||||
| 27 | + | if win != nil && win.win != nil { | |||
| 28 | + | win.win.AddController(controller) | |||
| 29 | + | } | |||
| 30 | + | ||||
| 16 | return &WindowShortcuts{ | 31 | return &WindowShortcuts{ | ||
| 17 | - | window: win, | 32 | + | window: win, |
| 18 | - | shortcuts: make(map[string]*gtk.ShortcutAction), | 33 | + | shortcuts: make(map[string]*gtk.Shortcut), |
| 34 | + | controller: controller, | |||
| 19 | } | 35 | } | ||
| 20 | } | 36 | } | ||
| 21 | 37 | ||||
| @@ -39,10 +55,107 @@ func (w *Window) InitializeGlobalShortcuts(shortcuts map[string]func()) { | |||||
| 39 | 55 | ||||
| 40 | // RegisterShortcut registers a keyboard shortcut | 56 | // RegisterShortcut registers a keyboard shortcut | ||
| 41 | func (ws *WindowShortcuts) RegisterShortcut(accelerator string, handler func()) { | 57 | func (ws *WindowShortcuts) RegisterShortcut(accelerator string, handler func()) { | ||
| 42 | - | if ws == nil { | 58 | + | if ws == nil || ws.controller == nil { |
| 43 | return | 59 | return | ||
| 44 | } | 60 | } | ||
| 45 | 61 | ||||
| 46 | - | // TODO: Implement proper shortcut registration using GTK4's shortcut system | 62 | + | // Convert to GTK format (e.g., "ctrl+plus" -> "<Control>plus") |
| 47 | - | // For now, this is a placeholder | 63 | + | gtkFormat := convertToGtkFormat(accelerator) |
| 64 | + | if gtkFormat == "" { | |||
| 65 | + | log.Printf("[shortcuts] Invalid key format: %s", accelerator) | |||
| 66 | + | return | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | // Create a callback action that will trigger our handler | |||
| 70 | + | // ShortcutFunc signature: func(widget Widgetter, args *glib.Variant) (ok bool) | |||
| 71 | + | action := gtk.NewCallbackAction(func(widget gtk.Widgetter, args *glib.Variant) bool { | |||
| 72 | + | if handler != nil { | |||
| 73 | + | log.Printf("[shortcuts] Shortcut triggered: %s", accelerator) | |||
| 74 | + | handler() | |||
| 75 | + | } | |||
| 76 | + | // Return false to allow event propagation to WebView | |||
| 77 | + | // This lets JavaScript KeyboardService also handle events if needed | |||
| 78 | + | return false | |||
| 79 | + | }) | |||
| 80 | + | ||||
| 81 | + | // Parse the accelerator string into a GDK key trigger | |||
| 82 | + | // GTK4 uses gtk.ShortcutTrigger for key combinations | |||
| 83 | + | trigger := gtk.NewShortcutTriggerParseString(gtkFormat) | |||
| 84 | + | if trigger == nil { | |||
| 85 | + | log.Printf("[shortcuts] Failed to parse trigger: %s", gtkFormat) | |||
| 86 | + | return | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // Create the shortcut with trigger and action | |||
| 90 | + | shortcut := gtk.NewShortcut(trigger, action) | |||
| 91 | + | ||||
| 92 | + | // Add shortcut to the shared controller | |||
| 93 | + | ws.controller.AddShortcut(shortcut) | |||
| 94 | + | ||||
| 95 | + | // Store the shortcut for potential cleanup | |||
| 96 | + | ws.shortcuts[accelerator] = shortcut | |||
| 97 | + | ||||
| 98 | + | log.Printf("[shortcuts] Registered: %s -> %s", accelerator, gtkFormat) | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | // convertToGtkFormat converts common key formats to GTK shortcut format | |||
| 102 | + | // Examples: "ctrl+plus" -> "<Control>plus", "ctrl+l" -> "<Control>l" | |||
| 103 | + | func convertToGtkFormat(key string) string { | |||
| 104 | + | key = strings.ToLower(key) | |||
| 105 | + | ||||
| 106 | + | // Handle cmdorctrl -> Control | |||
| 107 | + | key = strings.ReplaceAll(key, "cmdorctrl+", "ctrl+") | |||
| 108 | + | key = strings.ReplaceAll(key, "cmd+", "ctrl+") | |||
| 109 | + | ||||
| 110 | + | // Split by + to get components | |||
| 111 | + | components := strings.Split(key, "+") | |||
| 112 | + | if len(components) == 0 { | |||
| 113 | + | return "" | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | var parts []string | |||
| 117 | + | for i, part := range components { | |||
| 118 | + | if i == len(components)-1 { | |||
| 119 | + | // Last part is the actual key | |||
| 120 | + | switch part { | |||
| 121 | + | case "f12": | |||
| 122 | + | parts = append(parts, "F12") | |||
| 123 | + | case "f5": | |||
| 124 | + | parts = append(parts, "F5") | |||
| 125 | + | case "plus": | |||
| 126 | + | parts = append(parts, "plus") | |||
| 127 | + | case "equal": | |||
| 128 | + | parts = append(parts, "equal") | |||
| 129 | + | case "minus": | |||
| 130 | + | parts = append(parts, "minus") | |||
| 131 | + | case "0": | |||
| 132 | + | parts = append(parts, "0") | |||
| 133 | + | case "left", "arrowleft": | |||
| 134 | + | parts = append(parts, "Left") | |||
| 135 | + | case "right", "arrowright": | |||
| 136 | + | parts = append(parts, "Right") | |||
| 137 | + | case "up", "arrowup": | |||
| 138 | + | parts = append(parts, "Up") | |||
| 139 | + | case "down", "arrowdown": | |||
| 140 | + | parts = append(parts, "Down") | |||
| 141 | + | default: | |||
| 142 | + | // Single letter keys like l, f, c, w, etc. | |||
| 143 | + | parts = append(parts, part) | |||
| 144 | + | } | |||
| 145 | + | } else { | |||
| 146 | + | // Modifier keys | |||
| 147 | + | switch part { | |||
| 148 | + | case "ctrl": | |||
| 149 | + | parts = append(parts, "<Control>") | |||
| 150 | + | case "shift": | |||
| 151 | + | parts = append(parts, "<Shift>") | |||
| 152 | + | case "alt": | |||
| 153 | + | parts = append(parts, "<Alt>") | |||
| 154 | + | case "super", "meta": | |||
| 155 | + | parts = append(parts, "<Super>") | |||
| 156 | + | } | |||
| 157 | + | } | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | return strings.Join(parts, "") | |||
| 48 | } | 161 | } | ||
M pkg/webkit/uri_scheme.go +47 -11
| @@ -1,26 +1,62 @@ | |||
| 1 | 1 | package webkit | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "log" | |
| 6 | + | ||
| 4 | 7 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | |
| 5 | 8 | ) | |
| 6 | 9 | ||
| 7 | 10 | // URISchemeRequestCallback is called when a custom URI scheme is requested | |
| 8 | 11 | type URISchemeRequestCallback func(request *webkit.URISchemeRequest) | |
| 9 | 12 | ||
| 10 | - | // SetURISchemeResolver registers a custom URI scheme handler | |
| 11 | - | func SetURISchemeResolver(scheme string, callback URISchemeRequestCallback) { | |
| 12 | - | // Get the default web context | |
| 13 | - | ctx := webkit.WebContextGetDefault() | |
| 13 | + | // URISchemeHandler stores registered URI scheme handlers | |
| 14 | + | type URISchemeHandler struct { | |
| 15 | + | scheme string | |
| 16 | + | callback URISchemeRequestCallback | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | var ( | |
| 20 | + | // pendingURISchemeHandlers stores handlers to be registered before WebView creation | |
| 21 | + | pendingURISchemeHandlers []*URISchemeHandler | |
| 22 | + | ) | |
| 23 | + | ||
| 24 | + | // RegisterURIScheme registers a custom URI scheme handler | |
| 25 | + | // In WebKitGTK 6.0, this must be called BEFORE creating any WebViews | |
| 26 | + | // The actual registration happens in InitPersistentSession | |
| 27 | + | func RegisterURIScheme(scheme string, callback URISchemeRequestCallback) { | |
| 28 | + | log.Printf("[webkit] Registering URI scheme handler for: %s", scheme) | |
| 29 | + | pendingURISchemeHandlers = append(pendingURISchemeHandlers, &URISchemeHandler{ | |
| 30 | + | scheme: scheme, | |
| 31 | + | callback: callback, | |
| 32 | + | }) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // ApplyURISchemeHandlers registers all pending URI schemes on the WebContext | |
| 36 | + | // This should be called after creating the first WebView | |
| 37 | + | func ApplyURISchemeHandlers(view *webkit.WebView) error { | |
| 38 | + | if view == nil { | |
| 39 | + | return fmt.Errorf("webview is nil") | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | ctx := view.Context() | |
| 14 | 43 | if ctx == nil { | |
| 15 | - | return | |
| 44 | + | return fmt.Errorf("webcontext is nil") | |
| 16 | 45 | } | |
| 17 | 46 | ||
| 18 | - | // Register the URI scheme | |
| 19 | - | ctx.RegisterURIScheme(scheme, func(req *webkit.URISchemeRequest) { | |
| 20 | - | if callback != nil { | |
| 21 | - | callback(req) | |
| 22 | - | } | |
| 23 | - | }) | |
| 47 | + | for _, handler := range pendingURISchemeHandlers { | |
| 48 | + | log.Printf("[webkit] Registering URI scheme on WebContext: %s", handler.scheme) | |
| 49 | + | ||
| 50 | + | // Capture handler in closure | |
| 51 | + | callback := handler.callback | |
| 52 | + | ctx.RegisterURIScheme(handler.scheme, func(req *webkit.URISchemeRequest) { | |
| 53 | + | if callback != nil { | |
| 54 | + | callback(req) | |
| 55 | + | } | |
| 56 | + | }) | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | return nil | |
| 24 | 60 | } | |
| 25 | 61 | ||
| 26 | 62 | // FinishURISchemeRequest finishes a URI scheme request with data | |
| @@ -1,26 +1,62 @@ | |||||
| 1 | package webkit | 1 | package webkit | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "fmt" | |||
| 5 | + | "log" | |||
| 6 | + | ||||
| 4 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | 7 | webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6" | ||
| 5 | ) | 8 | ) | ||
| 6 | 9 | ||||
| 7 | // URISchemeRequestCallback is called when a custom URI scheme is requested | 10 | // URISchemeRequestCallback is called when a custom URI scheme is requested | ||
| 8 | type URISchemeRequestCallback func(request *webkit.URISchemeRequest) | 11 | type URISchemeRequestCallback func(request *webkit.URISchemeRequest) | ||
| 9 | 12 | ||||
| 10 | - | // SetURISchemeResolver registers a custom URI scheme handler | 13 | + | // URISchemeHandler stores registered URI scheme handlers |
| 11 | - | func SetURISchemeResolver(scheme string, callback URISchemeRequestCallback) { | 14 | + | type URISchemeHandler struct { |
| 12 | - | // Get the default web context | 15 | + | scheme string |
| 13 | - | ctx := webkit.WebContextGetDefault() | 16 | + | callback URISchemeRequestCallback |
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | var ( | |||
| 20 | + | // pendingURISchemeHandlers stores handlers to be registered before WebView creation | |||
| 21 | + | pendingURISchemeHandlers []*URISchemeHandler | |||
| 22 | + | ) | |||
| 23 | + | ||||
| 24 | + | // RegisterURIScheme registers a custom URI scheme handler | |||
| 25 | + | // In WebKitGTK 6.0, this must be called BEFORE creating any WebViews | |||
| 26 | + | // The actual registration happens in InitPersistentSession | |||
| 27 | + | func RegisterURIScheme(scheme string, callback URISchemeRequestCallback) { | |||
| 28 | + | log.Printf("[webkit] Registering URI scheme handler for: %s", scheme) | |||
| 29 | + | pendingURISchemeHandlers = append(pendingURISchemeHandlers, &URISchemeHandler{ | |||
| 30 | + | scheme: scheme, | |||
| 31 | + | callback: callback, | |||
| 32 | + | }) | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // ApplyURISchemeHandlers registers all pending URI schemes on the WebContext | |||
| 36 | + | // This should be called after creating the first WebView | |||
| 37 | + | func ApplyURISchemeHandlers(view *webkit.WebView) error { | |||
| 38 | + | if view == nil { | |||
| 39 | + | return fmt.Errorf("webview is nil") | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | ctx := view.Context() | |||
| 14 | if ctx == nil { | 43 | if ctx == nil { | ||
| 15 | - | return | 44 | + | return fmt.Errorf("webcontext is nil") |
| 16 | } | 45 | } | ||
| 17 | 46 | ||||
| 18 | - | // Register the URI scheme | 47 | + | for _, handler := range pendingURISchemeHandlers { |
| 19 | - | ctx.RegisterURIScheme(scheme, func(req *webkit.URISchemeRequest) { | 48 | + | log.Printf("[webkit] Registering URI scheme on WebContext: %s", handler.scheme) |
| 20 | - | if callback != nil { | 49 | + | |
| 21 | - | callback(req) | 50 | + | // Capture handler in closure |
| 22 | - | } | 51 | + | callback := handler.callback |
| 23 | - | }) | 52 | + | ctx.RegisterURIScheme(handler.scheme, func(req *webkit.URISchemeRequest) { |
| 53 | + | if callback != nil { | |||
| 54 | + | callback(req) | |||
| 55 | + | } | |||
| 56 | + | }) | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | return nil | |||
| 24 | } | 60 | } | ||
| 25 | 61 | ||||
| 26 | // FinishURISchemeRequest finishes a URI scheme request with data | 62 | // FinishURISchemeRequest finishes a URI scheme request with data | ||
M pkg/webkit/user_content.go +16 -2
| @@ -10,7 +10,7 @@ import ( | |||
| 10 | 10 | ||
| 11 | 11 | // SetupUserContentManager configures UserContentManager for the WebView | |
| 12 | 12 | // This injects GUI scripts at document-start and registers message handlers | |
| 13 | - | func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string) error { | |
| 13 | + | func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, webviewID uint64) error { | |
| 14 | 14 | if view == nil { | |
| 15 | 15 | return nil | |
| 16 | 16 | } | |
| @@ -22,7 +22,21 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string) | |||
| 22 | 22 | return nil | |
| 23 | 23 | } | |
| 24 | 24 | ||
| 25 | - | // Inject GTK theme detection FIRST, before color-scheme script | |
| 25 | + | // Inject webview ID FIRST, so GUI scripts can access it immediately | |
| 26 | + | webviewIDScript := fmt.Sprintf(` | |
| 27 | + | window.__dumber_webview_id = "%d"; | |
| 28 | + | console.log('[webkit] WebView ID set in JavaScript:', window.__dumber_webview_id); | |
| 29 | + | `, webviewID) | |
| 30 | + | ucm.AddScript(webkit.NewUserScript( | |
| 31 | + | webviewIDScript, | |
| 32 | + | webkit.UserContentInjectAllFrames, | |
| 33 | + | webkit.UserScriptInjectAtDocumentStart, | |
| 34 | + | nil, | |
| 35 | + | nil, | |
| 36 | + | )) | |
| 37 | + | log.Printf("[webkit] Injected webview ID script for ID: %d", webviewID) | |
| 38 | + | ||
| 39 | + | // Inject GTK theme detection SECOND, before color-scheme script | |
| 26 | 40 | // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set | |
| 27 | 41 | prefersDark := PrefersDarkTheme() | |
| 28 | 42 | gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark) | |
| @@ -10,7 +10,7 @@ import ( | |||||
| 10 | 10 | ||||
| 11 | // SetupUserContentManager configures UserContentManager for the WebView | 11 | // SetupUserContentManager configures UserContentManager for the WebView | ||
| 12 | // This injects GUI scripts at document-start and registers message handlers | 12 | // This injects GUI scripts at document-start and registers message handlers | ||
| 13 | - | func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string) error { | 13 | + | func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string, webviewID uint64) error { |
| 14 | if view == nil { | 14 | if view == nil { | ||
| 15 | return nil | 15 | return nil | ||
| 16 | } | 16 | } | ||
| @@ -22,7 +22,21 @@ func SetupUserContentManager(view *webkit.WebView, appearanceConfigJSON string) | |||||
| 22 | return nil | 22 | return nil | ||
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | - | // Inject GTK theme detection FIRST, before color-scheme script | 25 | + | // Inject webview ID FIRST, so GUI scripts can access it immediately |
| 26 | + | webviewIDScript := fmt.Sprintf(` | |||
| 27 | + | window.__dumber_webview_id = "%d"; | |||
| 28 | + | console.log('[webkit] WebView ID set in JavaScript:', window.__dumber_webview_id); | |||
| 29 | + | `, webviewID) | |||
| 30 | + | ucm.AddScript(webkit.NewUserScript( | |||
| 31 | + | webviewIDScript, | |||
| 32 | + | webkit.UserContentInjectAllFrames, | |||
| 33 | + | webkit.UserScriptInjectAtDocumentStart, | |||
| 34 | + | nil, | |||
| 35 | + | nil, | |||
| 36 | + | )) | |||
| 37 | + | log.Printf("[webkit] Injected webview ID script for ID: %d", webviewID) | |||
| 38 | + | ||||
| 39 | + | // Inject GTK theme detection SECOND, before color-scheme script | |||
| 26 | // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set | 40 | // The color-scheme.ts expects window.__dumber_gtk_prefers_dark to be set | ||
| 27 | prefersDark := PrefersDarkTheme() | 41 | prefersDark := PrefersDarkTheme() | ||
| 28 | gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark) | 42 | gtkThemeScript := fmt.Sprintf(`window.__dumber_gtk_prefers_dark = %t;`, prefersDark) | ||
M pkg/webkit/webview.go +48 -8
| @@ -1,7 +1,9 @@ | |||
| 1 | 1 | package webkit | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "encoding/json" | |
| 4 | 5 | "fmt" | |
| 6 | + | "log" | |
| 5 | 7 | "strconv" | |
| 6 | 8 | "sync" | |
| 7 | 9 | "sync/atomic" | |
| @@ -46,6 +48,10 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 46 | 48 | cfg = GetDefaultConfig() | |
| 47 | 49 | } | |
| 48 | 50 | ||
| 51 | + | // Generate unique ID immediately | |
| 52 | + | id := atomic.AddUint64(&viewIDCounter, 1) | |
| 53 | + | log.Printf("[webkit] Generated WebView ID: %d", id) | |
| 54 | + | ||
| 49 | 55 | InitMainThread() | |
| 50 | 56 | ||
| 51 | 57 | // Create GTK Window | |
| @@ -91,15 +97,12 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 91 | 97 | cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqlite) | |
| 92 | 98 | cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty) | |
| 93 | 99 | ||
| 94 | - | // Setup UserContentManager and inject GUI scripts | |
| 100 | + | // Setup UserContentManager and inject GUI scripts (including webview ID) | |
| 95 | 101 | // This must be done BEFORE any pages are loaded | |
| 96 | - | if err := SetupUserContentManager(wkView, cfg.AppearanceConfigJSON); err != nil { | |
| 102 | + | if err := SetupUserContentManager(wkView, cfg.AppearanceConfigJSON, id); err != nil { | |
| 97 | 103 | return nil, fmt.Errorf("failed to setup user content manager: %w", err) | |
| 98 | 104 | } | |
| 99 | 105 | ||
| 100 | - | // Generate unique ID | |
| 101 | - | id := atomic.AddUint64(&viewIDCounter, 1) | |
| 102 | - | ||
| 103 | 106 | wv := &WebView{ | |
| 104 | 107 | view: wkView, | |
| 105 | 108 | window: window, | |
| @@ -115,6 +118,9 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 115 | 118 | // Setup event handlers | |
| 116 | 119 | wv.setupEventHandlers() | |
| 117 | 120 | ||
| 121 | + | // Attach keyboard bridge to forward keyboard events to JavaScript | |
| 122 | + | wv.AttachKeyboardBridge() | |
| 123 | + | ||
| 118 | 124 | // Add WebView as child of window | |
| 119 | 125 | window.SetChild(wkView) | |
| 120 | 126 | ||
| @@ -193,6 +199,23 @@ func (w *WebView) setupEventHandlers() { | |||
| 193 | 199 | w.onClose() | |
| 194 | 200 | } | |
| 195 | 201 | }) | |
| 202 | + | ||
| 203 | + | // Script message received - connect to UserContentManager's script-message-received signal | |
| 204 | + | // This receives messages from JavaScript via webkit.messageHandlers.dumber.postMessage() | |
| 205 | + | // The signal name includes the handler name as a detail: "script-message-received::dumber" | |
| 206 | + | ucm := w.view.UserContentManager() | |
| 207 | + | if ucm != nil { | |
| 208 | + | // GTK signals pass the sender as the first parameter, the actual signal data as subsequent parameters | |
| 209 | + | ucm.Connect("script-message-received::dumber", func(sender interface{}, jscValue interface{}) { | |
| 210 | + | if w.onScriptMessage != nil && jscValue != nil { | |
| 211 | + | // Convert JSCValue to string using gotk4 javascriptcore bindings | |
| 212 | + | valueStr := JSCValueToString(jscValue) | |
| 213 | + | if valueStr != "" { | |
| 214 | + | w.onScriptMessage(valueStr) | |
| 215 | + | } | |
| 216 | + | } | |
| 217 | + | }) | |
| 218 | + | } | |
| 196 | 219 | } | |
| 197 | 220 | ||
| 198 | 221 | // LoadURL loads the given URL in the WebView | |
| @@ -524,10 +547,27 @@ func (w *WebView) InjectScript(script string) error { | |||
| 524 | 547 | ||
| 525 | 548 | // DispatchCustomEvent dispatches a custom event via JavaScript | |
| 526 | 549 | func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error { | |
| 527 | - | // TODO: Implement proper event dispatching with data serialization | |
| 550 | + | // Serialize data to JSON | |
| 551 | + | jsonData, err := json.Marshal(data) | |
| 552 | + | if err != nil { | |
| 553 | + | return fmt.Errorf("failed to marshal event data: %w", err) | |
| 554 | + | } | |
| 555 | + | ||
| 556 | + | log.Printf("[webkit] Dispatching event '%s' to WebView ID %d with data: %s", eventName, w.id, string(jsonData)) | |
| 557 | + | ||
| 558 | + | // Use document.dispatchEvent (not window) so events cross JavaScript world boundaries | |
| 559 | + | // The GUI scripts run in an isolated world but share the same Document object | |
| 528 | 560 | script := fmt.Sprintf(` | |
| 529 | - | window.dispatchEvent(new CustomEvent('%s', { detail: %v })); | |
| 530 | - | `, eventName, data) | |
| 561 | + | (function() { | |
| 562 | + | try { | |
| 563 | + | var detail = %s; | |
| 564 | + | console.log('[dumber] Dispatching event: %s', detail); | |
| 565 | + | document.dispatchEvent(new CustomEvent('%s', { detail: detail })); | |
| 566 | + | } catch (e) { | |
| 567 | + | console.error('[dumber] Failed to dispatch %s', e); | |
| 568 | + | } | |
| 569 | + | })(); | |
| 570 | + | `, string(jsonData), eventName, eventName, eventName) | |
| 531 | 571 | return w.InjectScript(script) | |
| 532 | 572 | } | |
| 533 | 573 | ||
| @@ -1,7 +1,9 @@ | |||||
| 1 | package webkit | 1 | package webkit | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "encoding/json" | |||
| 4 | "fmt" | 5 | "fmt" | ||
| 6 | + | "log" | |||
| 5 | "strconv" | 7 | "strconv" | ||
| 6 | "sync" | 8 | "sync" | ||
| 7 | "sync/atomic" | 9 | "sync/atomic" | ||
| @@ -46,6 +48,10 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 46 | cfg = GetDefaultConfig() | 48 | cfg = GetDefaultConfig() | ||
| 47 | } | 49 | } | ||
| 48 | 50 | ||||
| 51 | + | // Generate unique ID immediately | |||
| 52 | + | id := atomic.AddUint64(&viewIDCounter, 1) | |||
| 53 | + | log.Printf("[webkit] Generated WebView ID: %d", id) | |||
| 54 | + | ||||
| 49 | InitMainThread() | 55 | InitMainThread() | ||
| 50 | 56 | ||||
| 51 | // Create GTK Window | 57 | // Create GTK Window | ||
| @@ -91,15 +97,12 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 91 | cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqlite) | 97 | cookieManager.SetPersistentStorage(cookiePath, webkit.CookiePersistentStorageSqlite) | ||
| 92 | cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty) | 98 | cookieManager.SetAcceptPolicy(webkit.CookiePolicyAcceptNoThirdParty) | ||
| 93 | 99 | ||||
| 94 | - | // Setup UserContentManager and inject GUI scripts | 100 | + | // Setup UserContentManager and inject GUI scripts (including webview ID) |
| 95 | // This must be done BEFORE any pages are loaded | 101 | // This must be done BEFORE any pages are loaded | ||
| 96 | - | if err := SetupUserContentManager(wkView, cfg.AppearanceConfigJSON); err != nil { | 102 | + | if err := SetupUserContentManager(wkView, cfg.AppearanceConfigJSON, id); err != nil { |
| 97 | return nil, fmt.Errorf("failed to setup user content manager: %w", err) | 103 | return nil, fmt.Errorf("failed to setup user content manager: %w", err) | ||
| 98 | } | 104 | } | ||
| 99 | 105 | ||||
| 100 | - | // Generate unique ID | |||
| 101 | - | id := atomic.AddUint64(&viewIDCounter, 1) | |||
| 102 | - | ||||
| 103 | wv := &WebView{ | 106 | wv := &WebView{ | ||
| 104 | view: wkView, | 107 | view: wkView, | ||
| 105 | window: window, | 108 | window: window, | ||
| @@ -115,6 +118,9 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 115 | // Setup event handlers | 118 | // Setup event handlers | ||
| 116 | wv.setupEventHandlers() | 119 | wv.setupEventHandlers() | ||
| 117 | 120 | ||||
| 121 | + | // Attach keyboard bridge to forward keyboard events to JavaScript | |||
| 122 | + | wv.AttachKeyboardBridge() | |||
| 123 | + | ||||
| 118 | // Add WebView as child of window | 124 | // Add WebView as child of window | ||
| 119 | window.SetChild(wkView) | 125 | window.SetChild(wkView) | ||
| 120 | 126 | ||||
| @@ -193,6 +199,23 @@ func (w *WebView) setupEventHandlers() { | |||||
| 193 | w.onClose() | 199 | w.onClose() | ||
| 194 | } | 200 | } | ||
| 195 | }) | 201 | }) | ||
| 202 | + | ||||
| 203 | + | // Script message received - connect to UserContentManager's script-message-received signal | |||
| 204 | + | // This receives messages from JavaScript via webkit.messageHandlers.dumber.postMessage() | |||
| 205 | + | // The signal name includes the handler name as a detail: "script-message-received::dumber" | |||
| 206 | + | ucm := w.view.UserContentManager() | |||
| 207 | + | if ucm != nil { | |||
| 208 | + | // GTK signals pass the sender as the first parameter, the actual signal data as subsequent parameters | |||
| 209 | + | ucm.Connect("script-message-received::dumber", func(sender interface{}, jscValue interface{}) { | |||
| 210 | + | if w.onScriptMessage != nil && jscValue != nil { | |||
| 211 | + | // Convert JSCValue to string using gotk4 javascriptcore bindings | |||
| 212 | + | valueStr := JSCValueToString(jscValue) | |||
| 213 | + | if valueStr != "" { | |||
| 214 | + | w.onScriptMessage(valueStr) | |||
| 215 | + | } | |||
| 216 | + | } | |||
| 217 | + | }) | |||
| 218 | + | } | |||
| 196 | } | 219 | } | ||
| 197 | 220 | ||||
| 198 | // LoadURL loads the given URL in the WebView | 221 | // LoadURL loads the given URL in the WebView | ||
| @@ -524,10 +547,27 @@ func (w *WebView) InjectScript(script string) error { | |||||
| 524 | 547 | ||||
| 525 | // DispatchCustomEvent dispatches a custom event via JavaScript | 548 | // DispatchCustomEvent dispatches a custom event via JavaScript | ||
| 526 | func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error { | 549 | func (w *WebView) DispatchCustomEvent(eventName string, data interface{}) error { | ||
| 527 | - | // TODO: Implement proper event dispatching with data serialization | 550 | + | // Serialize data to JSON |
| 551 | + | jsonData, err := json.Marshal(data) | |||
| 552 | + | if err != nil { | |||
| 553 | + | return fmt.Errorf("failed to marshal event data: %w", err) | |||
| 554 | + | } | |||
| 555 | + | ||||
| 556 | + | log.Printf("[webkit] Dispatching event '%s' to WebView ID %d with data: %s", eventName, w.id, string(jsonData)) | |||
| 557 | + | ||||
| 558 | + | // Use document.dispatchEvent (not window) so events cross JavaScript world boundaries | |||
| 559 | + | // The GUI scripts run in an isolated world but share the same Document object | |||
| 528 | script := fmt.Sprintf(` | 560 | script := fmt.Sprintf(` | ||
| 529 | - | window.dispatchEvent(new CustomEvent('%s', { detail: %v })); | 561 | + | (function() { |
| 530 | - | `, eventName, data) | 562 | + | try { |
| 563 | + | var detail = %s; | |||
| 564 | + | console.log('[dumber] Dispatching event: %s', detail); | |||
| 565 | + | document.dispatchEvent(new CustomEvent('%s', { detail: detail })); | |||
| 566 | + | } catch (e) { | |||
| 567 | + | console.error('[dumber] Failed to dispatch %s', e); | |||
| 568 | + | } | |||
| 569 | + | })(); | |||
| 570 | + | `, string(jsonData), eventName, eventName, eventName) | |||
| 531 | return w.InjectScript(script) | 571 | return w.InjectScript(script) | ||
| 532 | } | 572 | } | ||
| 533 | 573 | ||||