fix(omnibox): implement webkit-level keyboard event isolation to prevent page interference
- commit
- d7fd74078c85f63bbb678567c9afed014a105032
- author
- bamen · · 2025-09-23 04:42 UTC
- parent
- df4e539
- signature
- unsigned
7 files · +204 -1
M CHANGELOG.md +1 -0
| @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. | |||
| 5 | 5 | ## [Unreleased] | |
| 6 | 6 | ||
| 7 | 7 | ### Fixed | |
| 8 | + | - **Omnibox keyboard event isolation**: Fixed keyboard event leakage from omnibox to underlying webpages that caused unintended page actions (e.g., typing 's' in omnibox triggering GitHub search). Implemented WebKit-level main-world event blocking that activates when omnibox opens, preventing page JavaScript from receiving keyboard events while preserving native GTK shortcuts (Ctrl+L, Ctrl+F, etc.) and omnibox functionality | |
| 8 | 9 | - **Active pane detection**: Replaced complex pane ID abstraction with persistent webview IDs, fixing omnibox unresponsiveness after navigation. Omnibox now works immediately after page navigation without requiring mouse movement | |
| 9 | 10 | - **Workspace architecture**: Simplified focus tracking by removing URI change handlers and pane ID injection complexity, improving reliability and reducing codebase by 85 lines | |
| 10 | 11 | - **WebView ID propagation**: Added webview ID request/response mechanism for stub builds to ensure JavaScript side knows its webview ID for proper focus validation | |
| @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. | |||||
| 5 | ## [Unreleased] | 5 | ## [Unreleased] | ||
| 6 | 6 | ||||
| 7 | ### Fixed | 7 | ### Fixed | ||
| 8 | + | - **Omnibox keyboard event isolation**: Fixed keyboard event leakage from omnibox to underlying webpages that caused unintended page actions (e.g., typing 's' in omnibox triggering GitHub search). Implemented WebKit-level main-world event blocking that activates when omnibox opens, preventing page JavaScript from receiving keyboard events while preserving native GTK shortcuts (Ctrl+L, Ctrl+F, etc.) and omnibox functionality | |||
| 8 | - **Active pane detection**: Replaced complex pane ID abstraction with persistent webview IDs, fixing omnibox unresponsiveness after navigation. Omnibox now works immediately after page navigation without requiring mouse movement | 9 | - **Active pane detection**: Replaced complex pane ID abstraction with persistent webview IDs, fixing omnibox unresponsiveness after navigation. Omnibox now works immediately after page navigation without requiring mouse movement | ||
| 9 | - **Workspace architecture**: Simplified focus tracking by removing URI change handlers and pane ID injection complexity, improving reliability and reducing codebase by 85 lines | 10 | - **Workspace architecture**: Simplified focus tracking by removing URI change handlers and pane ID injection complexity, improving reliability and reducing codebase by 85 lines | ||
| 10 | - **WebView ID propagation**: Added webview ID request/response mechanism for stub builds to ensure JavaScript side knows its webview ID for proper focus validation | 11 | - **WebView ID propagation**: Added webview ID request/response mechanism for stub builds to ensure JavaScript side knows its webview ID for proper focus validation | ||
M gui/package-lock.json +15 -0 generated file: collapsed
M gui/src/components/omnibox/Omnibox.svelte +37 -1
| @@ -116,13 +116,47 @@ | |||
| 116 | 116 | }; | |
| 117 | 117 | } | |
| 118 | 118 | ||
| 119 | - | // Focus input when component becomes visible | |
| 119 | + | // Focus input when component becomes visible and manage page event blocking | |
| 120 | 120 | $effect(() => { | |
| 121 | 121 | if (visible && inputElement) { | |
| 122 | 122 | inputElement.focus(); | |
| 123 | + | // Enable keyboard event blocking in main world when omnibox opens | |
| 124 | + | enablePageEventBlocking(); | |
| 125 | + | } else if (!visible) { | |
| 126 | + | // Disable keyboard event blocking when omnibox closes | |
| 127 | + | disablePageEventBlocking(); | |
| 123 | 128 | } | |
| 124 | 129 | }); | |
| 125 | 130 | ||
| 131 | + | // Functions to control main-world event blocking | |
| 132 | + | function enablePageEventBlocking() { | |
| 133 | + | try { | |
| 134 | + | // Send message to Go backend to enable keyboard blocking | |
| 135 | + | if (window.webkit?.messageHandlers?.dumber) { | |
| 136 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ | |
| 137 | + | type: 'keyboard_blocking', | |
| 138 | + | action: 'enable' | |
| 139 | + | })); | |
| 140 | + | } | |
| 141 | + | } catch (error) { | |
| 142 | + | console.warn('[omnibox] Failed to enable page event blocking:', error); | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | function disablePageEventBlocking() { | |
| 147 | + | try { | |
| 148 | + | // Send message to Go backend to disable keyboard blocking | |
| 149 | + | if (window.webkit?.messageHandlers?.dumber) { | |
| 150 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ | |
| 151 | + | type: 'keyboard_blocking', | |
| 152 | + | action: 'disable' | |
| 153 | + | })); | |
| 154 | + | } | |
| 155 | + | } catch (error) { | |
| 156 | + | console.warn('[omnibox] Failed to disable page event blocking:', error); | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | ||
| 126 | 160 | ||
| 127 | 161 | // Apply faded styling effect (matching original JS implementation) | |
| 128 | 162 | // NOTE: to avoid text becoming blurry while animating, we animate the | |
| @@ -218,6 +252,8 @@ | |||
| 218 | 252 | } | |
| 219 | 253 | } | |
| 220 | 254 | ||
| 255 | + | ||
| 256 | + | ||
| 221 | 257 | onMount(() => { | |
| 222 | 258 | syncTheme(); | |
| 223 | 259 | observeThemeChanges(); | |
| @@ -116,13 +116,47 @@ | |||||
| 116 | }; | 116 | }; | ||
| 117 | } | 117 | } | ||
| 118 | 118 | ||||
| 119 | - | // Focus input when component becomes visible | 119 | + | // Focus input when component becomes visible and manage page event blocking |
| 120 | $effect(() => { | 120 | $effect(() => { | ||
| 121 | if (visible && inputElement) { | 121 | if (visible && inputElement) { | ||
| 122 | inputElement.focus(); | 122 | inputElement.focus(); | ||
| 123 | + | // Enable keyboard event blocking in main world when omnibox opens | |||
| 124 | + | enablePageEventBlocking(); | |||
| 125 | + | } else if (!visible) { | |||
| 126 | + | // Disable keyboard event blocking when omnibox closes | |||
| 127 | + | disablePageEventBlocking(); | |||
| 123 | } | 128 | } | ||
| 124 | }); | 129 | }); | ||
| 125 | 130 | ||||
| 131 | + | // Functions to control main-world event blocking | |||
| 132 | + | function enablePageEventBlocking() { | |||
| 133 | + | try { | |||
| 134 | + | // Send message to Go backend to enable keyboard blocking | |||
| 135 | + | if (window.webkit?.messageHandlers?.dumber) { | |||
| 136 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ | |||
| 137 | + | type: 'keyboard_blocking', | |||
| 138 | + | action: 'enable' | |||
| 139 | + | })); | |||
| 140 | + | } | |||
| 141 | + | } catch (error) { | |||
| 142 | + | console.warn('[omnibox] Failed to enable page event blocking:', error); | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | function disablePageEventBlocking() { | |||
| 147 | + | try { | |||
| 148 | + | // Send message to Go backend to disable keyboard blocking | |||
| 149 | + | if (window.webkit?.messageHandlers?.dumber) { | |||
| 150 | + | window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({ | |||
| 151 | + | type: 'keyboard_blocking', | |||
| 152 | + | action: 'disable' | |||
| 153 | + | })); | |||
| 154 | + | } | |||
| 155 | + | } catch (error) { | |||
| 156 | + | console.warn('[omnibox] Failed to disable page event blocking:', error); | |||
| 157 | + | } | |||
| 158 | + | } | |||
| 159 | + | ||||
| 126 | 160 | ||||
| 127 | // Apply faded styling effect (matching original JS implementation) | 161 | // Apply faded styling effect (matching original JS implementation) | ||
| 128 | // NOTE: to avoid text becoming blurry while animating, we animate the | 162 | // NOTE: to avoid text becoming blurry while animating, we animate the | ||
| @@ -218,6 +252,8 @@ | |||||
| 218 | } | 252 | } | ||
| 219 | } | 253 | } | ||
| 220 | 254 | ||||
| 255 | + | ||||
| 256 | + | ||||
| 221 | onMount(() => { | 257 | onMount(() => { | ||
| 222 | syncTheme(); | 258 | syncTheme(); | ||
| 223 | observeThemeChanges(); | 259 | observeThemeChanges(); | ||
M gui/src/components/omnibox/OmniboxInput.svelte +6 -0
| @@ -69,11 +69,14 @@ | |||
| 69 | 69 | function handleKeyDown(event: KeyboardEvent) { | |
| 70 | 70 | switch (event.key) { | |
| 71 | 71 | case 'Escape': | |
| 72 | + | event.preventDefault(); | |
| 73 | + | event.stopPropagation(); | |
| 72 | 74 | omniboxStore.close(); | |
| 73 | 75 | break; | |
| 74 | 76 | ||
| 75 | 77 | case 'Enter': | |
| 76 | 78 | event.preventDefault(); | |
| 79 | + | event.stopPropagation(); | |
| 77 | 80 | handleEnterKey(event); | |
| 78 | 81 | break; | |
| 79 | 82 | ||
| @@ -85,6 +88,9 @@ | |||
| 85 | 88 | break; | |
| 86 | 89 | ||
| 87 | 90 | default: | |
| 91 | + | // For normal typing keys, only stop propagation to prevent page handlers | |
| 92 | + | // but don't prevent default so typing still works in the input | |
| 93 | + | event.stopPropagation(); | |
| 88 | 94 | // Any other key should restore full opacity | |
| 89 | 95 | omniboxStore.setFaded(false); | |
| 90 | 96 | break; | |
| @@ -69,11 +69,14 @@ | |||||
| 69 | function handleKeyDown(event: KeyboardEvent) { | 69 | function handleKeyDown(event: KeyboardEvent) { | ||
| 70 | switch (event.key) { | 70 | switch (event.key) { | ||
| 71 | case 'Escape': | 71 | case 'Escape': | ||
| 72 | + | event.preventDefault(); | |||
| 73 | + | event.stopPropagation(); | |||
| 72 | omniboxStore.close(); | 74 | omniboxStore.close(); | ||
| 73 | break; | 75 | break; | ||
| 74 | 76 | ||||
| 75 | case 'Enter': | 77 | case 'Enter': | ||
| 76 | event.preventDefault(); | 78 | event.preventDefault(); | ||
| 79 | + | event.stopPropagation(); | |||
| 77 | handleEnterKey(event); | 80 | handleEnterKey(event); | ||
| 78 | break; | 81 | break; | ||
| 79 | 82 | ||||
| @@ -85,6 +88,9 @@ | |||||
| 85 | break; | 88 | break; | ||
| 86 | 89 | ||||
| 87 | default: | 90 | default: | ||
| 91 | + | // For normal typing keys, only stop propagation to prevent page handlers | |||
| 92 | + | // but don't prevent default so typing still works in the input | |||
| 93 | + | event.stopPropagation(); | |||
| 88 | // Any other key should restore full opacity | 94 | // Any other key should restore full opacity | ||
| 89 | omniboxStore.setFaded(false); | 95 | omniboxStore.setFaded(false); | ||
| 90 | break; | 96 | break; | ||
M pkg/webkit/keyboard_cgo.go +42 -0
| @@ -408,6 +408,11 @@ func goOnUcmMessage(id C.ulong, json *C.char) { | |||
| 408 | 408 | return | |
| 409 | 409 | } | |
| 410 | 410 | ||
| 411 | + | // Handle keyboard blocking messages | |
| 412 | + | if handleKeyboardBlockingMessage(vw, goPayload) { | |
| 413 | + | return | |
| 414 | + | } | |
| 415 | + | ||
| 411 | 416 | vw.dispatchScriptMessage(goPayload) | |
| 412 | 417 | } | |
| 413 | 418 | ||
| @@ -684,3 +689,40 @@ func handleRenderingBackendMessage(payload string) bool { | |||
| 684 | 689 | ||
| 685 | 690 | return true | |
| 686 | 691 | } | |
| 692 | + | ||
| 693 | + | // handleKeyboardBlockingMessage handles keyboard blocking control messages from omnibox | |
| 694 | + | func handleKeyboardBlockingMessage(vw *WebView, payload string) bool { | |
| 695 | + | if vw == nil { | |
| 696 | + | return false | |
| 697 | + | } | |
| 698 | + | ||
| 699 | + | var msg struct { | |
| 700 | + | Type string `json:"type"` | |
| 701 | + | Action string `json:"action"` | |
| 702 | + | } | |
| 703 | + | ||
| 704 | + | if err := json.Unmarshal([]byte(payload), &msg); err != nil { | |
| 705 | + | return false | |
| 706 | + | } | |
| 707 | + | ||
| 708 | + | if msg.Type != "keyboard_blocking" { | |
| 709 | + | return false | |
| 710 | + | } | |
| 711 | + | ||
| 712 | + | switch msg.Action { | |
| 713 | + | case "enable": | |
| 714 | + | log.Printf("[webkit] Enabling page keyboard blocking for omnibox") | |
| 715 | + | if err := vw.EnablePageKeyboardBlocking(); err != nil { | |
| 716 | + | log.Printf("[webkit] Failed to enable keyboard blocking: %v", err) | |
| 717 | + | } | |
| 718 | + | case "disable": | |
| 719 | + | log.Printf("[webkit] Disabling page keyboard blocking") | |
| 720 | + | if err := vw.DisablePageKeyboardBlocking(); err != nil { | |
| 721 | + | log.Printf("[webkit] Failed to disable keyboard blocking: %v", err) | |
| 722 | + | } | |
| 723 | + | default: | |
| 724 | + | log.Printf("[webkit] Unknown keyboard blocking action: %s", msg.Action) | |
| 725 | + | } | |
| 726 | + | ||
| 727 | + | return true | |
| 728 | + | } | |
| @@ -408,6 +408,11 @@ func goOnUcmMessage(id C.ulong, json *C.char) { | |||||
| 408 | return | 408 | return | ||
| 409 | } | 409 | } | ||
| 410 | 410 | ||||
| 411 | + | // Handle keyboard blocking messages | |||
| 412 | + | if handleKeyboardBlockingMessage(vw, goPayload) { | |||
| 413 | + | return | |||
| 414 | + | } | |||
| 415 | + | ||||
| 411 | vw.dispatchScriptMessage(goPayload) | 416 | vw.dispatchScriptMessage(goPayload) | ||
| 412 | } | 417 | } | ||
| 413 | 418 | ||||
| @@ -684,3 +689,40 @@ func handleRenderingBackendMessage(payload string) bool { | |||||
| 684 | 689 | ||||
| 685 | return true | 690 | return true | ||
| 686 | } | 691 | } | ||
| 692 | + | ||||
| 693 | + | // handleKeyboardBlockingMessage handles keyboard blocking control messages from omnibox | |||
| 694 | + | func handleKeyboardBlockingMessage(vw *WebView, payload string) bool { | |||
| 695 | + | if vw == nil { | |||
| 696 | + | return false | |||
| 697 | + | } | |||
| 698 | + | ||||
| 699 | + | var msg struct { | |||
| 700 | + | Type string `json:"type"` | |||
| 701 | + | Action string `json:"action"` | |||
| 702 | + | } | |||
| 703 | + | ||||
| 704 | + | if err := json.Unmarshal([]byte(payload), &msg); err != nil { | |||
| 705 | + | return false | |||
| 706 | + | } | |||
| 707 | + | ||||
| 708 | + | if msg.Type != "keyboard_blocking" { | |||
| 709 | + | return false | |||
| 710 | + | } | |||
| 711 | + | ||||
| 712 | + | switch msg.Action { | |||
| 713 | + | case "enable": | |||
| 714 | + | log.Printf("[webkit] Enabling page keyboard blocking for omnibox") | |||
| 715 | + | if err := vw.EnablePageKeyboardBlocking(); err != nil { | |||
| 716 | + | log.Printf("[webkit] Failed to enable keyboard blocking: %v", err) | |||
| 717 | + | } | |||
| 718 | + | case "disable": | |||
| 719 | + | log.Printf("[webkit] Disabling page keyboard blocking") | |||
| 720 | + | if err := vw.DisablePageKeyboardBlocking(); err != nil { | |||
| 721 | + | log.Printf("[webkit] Failed to disable keyboard blocking: %v", err) | |||
| 722 | + | } | |||
| 723 | + | default: | |||
| 724 | + | log.Printf("[webkit] Unknown keyboard blocking action: %s", msg.Action) | |||
| 725 | + | } | |||
| 726 | + | ||||
| 727 | + | return true | |||
| 728 | + | } | |||
M pkg/webkit/shortcuts_cgo.go +102 -0
| @@ -5,6 +5,7 @@ package webkit | |||
| 5 | 5 | /* | |
| 6 | 6 | #cgo pkg-config: gtk4 webkitgtk-6.0 | |
| 7 | 7 | #include <gtk/gtk.h> | |
| 8 | + | #include <webkit/webkit.h> | |
| 8 | 9 | #include <stdint.h> | |
| 9 | 10 | #include <stdlib.h> | |
| 10 | 11 | ||
| @@ -142,3 +143,104 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err | |||
| 142 | 143 | ||
| 143 | 144 | return nil | |
| 144 | 145 | } | |
| 146 | + | ||
| 147 | + | // Keyboard event blocking functionality for omnibox isolation | |
| 148 | + | ||
| 149 | + | // keyboardBlockerScript holds reference to the main-world keyboard blocking script | |
| 150 | + | var keyboardBlockerScript *C.WebKitUserScript = nil | |
| 151 | + | ||
| 152 | + | // EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events | |
| 153 | + | // This prevents page JavaScript from receiving keyboard events while omnibox is active | |
| 154 | + | func (w *WebView) EnablePageKeyboardBlocking() error { | |
| 155 | + | if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil { | |
| 156 | + | return ErrWebViewNotInitialized | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | // Remove any existing blocker script first | |
| 160 | + | w.DisablePageKeyboardBlocking() | |
| 161 | + | ||
| 162 | + | // Script to block all keyboard and related events in the main world | |
| 163 | + | blockingScript := ` | |
| 164 | + | (() => { | |
| 165 | + | // Store original state to restore later | |
| 166 | + | if (!window.__dumber_event_blocker_installed) { | |
| 167 | + | window.__dumber_event_blocker_installed = true; | |
| 168 | + | ||
| 169 | + | // List of events to block | |
| 170 | + | const eventTypes = [ | |
| 171 | + | 'keydown', 'keyup', 'keypress', | |
| 172 | + | 'input', 'beforeinput', | |
| 173 | + | 'compositionstart', 'compositionupdate', 'compositionend' | |
| 174 | + | ]; | |
| 175 | + | ||
| 176 | + | // Block all keyboard-related events at capture phase | |
| 177 | + | const blockEvent = (e) => { | |
| 178 | + | e.stopImmediatePropagation(); | |
| 179 | + | e.preventDefault(); | |
| 180 | + | e.stopPropagation(); | |
| 181 | + | return false; | |
| 182 | + | }; | |
| 183 | + | ||
| 184 | + | // Install capture-phase event blockers | |
| 185 | + | eventTypes.forEach(type => { | |
| 186 | + | document.addEventListener(type, blockEvent, true); | |
| 187 | + | }); | |
| 188 | + | ||
| 189 | + | // Store cleanup function | |
| 190 | + | window.__dumber_cleanup_event_blocker = () => { | |
| 191 | + | eventTypes.forEach(type => { | |
| 192 | + | document.removeEventListener(type, blockEvent, true); | |
| 193 | + | }); | |
| 194 | + | delete window.__dumber_event_blocker_installed; | |
| 195 | + | delete window.__dumber_cleanup_event_blocker; | |
| 196 | + | }; | |
| 197 | + | ||
| 198 | + | console.log('[dumber] Page keyboard event blocking enabled'); | |
| 199 | + | } | |
| 200 | + | })(); | |
| 201 | + | ` | |
| 202 | + | ||
| 203 | + | cScript := C.CString(blockingScript) | |
| 204 | + | defer C.free(unsafe.Pointer(cScript)) | |
| 205 | + | ||
| 206 | + | // Inject into main world (nil world parameter = main world) | |
| 207 | + | keyboardBlockerScript = C.webkit_user_script_new( | |
| 208 | + | (*C.gchar)(cScript), | |
| 209 | + | C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, | |
| 210 | + | C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, | |
| 211 | + | nil, nil) // nil = main world | |
| 212 | + | ||
| 213 | + | if keyboardBlockerScript == nil { | |
| 214 | + | return ErrNotImplemented | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | C.webkit_user_content_manager_add_script(w.native.ucm, keyboardBlockerScript) | |
| 218 | + | log.Printf("[webkit] Page keyboard event blocking enabled") | |
| 219 | + | return nil | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | // DisablePageKeyboardBlocking removes the keyboard blocking script and restores normal page behavior | |
| 223 | + | func (w *WebView) DisablePageKeyboardBlocking() error { | |
| 224 | + | if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil { | |
| 225 | + | return ErrWebViewNotInitialized | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | // Execute cleanup script to remove event listeners | |
| 229 | + | cleanupScript := ` | |
| 230 | + | if (window.__dumber_cleanup_event_blocker) { | |
| 231 | + | window.__dumber_cleanup_event_blocker(); | |
| 232 | + | console.log('[dumber] Page keyboard event blocking disabled'); | |
| 233 | + | } | |
| 234 | + | ` | |
| 235 | + | w.InjectScript(cleanupScript) | |
| 236 | + | ||
| 237 | + | // Remove the user script if it exists | |
| 238 | + | if keyboardBlockerScript != nil { | |
| 239 | + | C.webkit_user_content_manager_remove_script(w.native.ucm, keyboardBlockerScript) | |
| 240 | + | C.webkit_user_script_unref(keyboardBlockerScript) | |
| 241 | + | keyboardBlockerScript = nil | |
| 242 | + | log.Printf("[webkit] Keyboard blocker script removed") | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | return nil | |
| 246 | + | } | |
| @@ -5,6 +5,7 @@ package webkit | |||||
| 5 | /* | 5 | /* | ||
| 6 | #cgo pkg-config: gtk4 webkitgtk-6.0 | 6 | #cgo pkg-config: gtk4 webkitgtk-6.0 | ||
| 7 | #include <gtk/gtk.h> | 7 | #include <gtk/gtk.h> | ||
| 8 | + | #include <webkit/webkit.h> | |||
| 8 | #include <stdint.h> | 9 | #include <stdint.h> | ||
| 9 | #include <stdlib.h> | 10 | #include <stdlib.h> | ||
| 10 | 11 | ||||
| @@ -142,3 +143,104 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err | |||||
| 142 | 143 | ||||
| 143 | return nil | 144 | return nil | ||
| 144 | } | 145 | } | ||
| 146 | + | ||||
| 147 | + | // Keyboard event blocking functionality for omnibox isolation | |||
| 148 | + | ||||
| 149 | + | // keyboardBlockerScript holds reference to the main-world keyboard blocking script | |||
| 150 | + | var keyboardBlockerScript *C.WebKitUserScript = nil | |||
| 151 | + | ||||
| 152 | + | // EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events | |||
| 153 | + | // This prevents page JavaScript from receiving keyboard events while omnibox is active | |||
| 154 | + | func (w *WebView) EnablePageKeyboardBlocking() error { | |||
| 155 | + | if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil { | |||
| 156 | + | return ErrWebViewNotInitialized | |||
| 157 | + | } | |||
| 158 | + | ||||
| 159 | + | // Remove any existing blocker script first | |||
| 160 | + | w.DisablePageKeyboardBlocking() | |||
| 161 | + | ||||
| 162 | + | // Script to block all keyboard and related events in the main world | |||
| 163 | + | blockingScript := ` | |||
| 164 | + | (() => { | |||
| 165 | + | // Store original state to restore later | |||
| 166 | + | if (!window.__dumber_event_blocker_installed) { | |||
| 167 | + | window.__dumber_event_blocker_installed = true; | |||
| 168 | + | ||||
| 169 | + | // List of events to block | |||
| 170 | + | const eventTypes = [ | |||
| 171 | + | 'keydown', 'keyup', 'keypress', | |||
| 172 | + | 'input', 'beforeinput', | |||
| 173 | + | 'compositionstart', 'compositionupdate', 'compositionend' | |||
| 174 | + | ]; | |||
| 175 | + | ||||
| 176 | + | // Block all keyboard-related events at capture phase | |||
| 177 | + | const blockEvent = (e) => { | |||
| 178 | + | e.stopImmediatePropagation(); | |||
| 179 | + | e.preventDefault(); | |||
| 180 | + | e.stopPropagation(); | |||
| 181 | + | return false; | |||
| 182 | + | }; | |||
| 183 | + | ||||
| 184 | + | // Install capture-phase event blockers | |||
| 185 | + | eventTypes.forEach(type => { | |||
| 186 | + | document.addEventListener(type, blockEvent, true); | |||
| 187 | + | }); | |||
| 188 | + | ||||
| 189 | + | // Store cleanup function | |||
| 190 | + | window.__dumber_cleanup_event_blocker = () => { | |||
| 191 | + | eventTypes.forEach(type => { | |||
| 192 | + | document.removeEventListener(type, blockEvent, true); | |||
| 193 | + | }); | |||
| 194 | + | delete window.__dumber_event_blocker_installed; | |||
| 195 | + | delete window.__dumber_cleanup_event_blocker; | |||
| 196 | + | }; | |||
| 197 | + | ||||
| 198 | + | console.log('[dumber] Page keyboard event blocking enabled'); | |||
| 199 | + | } | |||
| 200 | + | })(); | |||
| 201 | + | ` | |||
| 202 | + | ||||
| 203 | + | cScript := C.CString(blockingScript) | |||
| 204 | + | defer C.free(unsafe.Pointer(cScript)) | |||
| 205 | + | ||||
| 206 | + | // Inject into main world (nil world parameter = main world) | |||
| 207 | + | keyboardBlockerScript = C.webkit_user_script_new( | |||
| 208 | + | (*C.gchar)(cScript), | |||
| 209 | + | C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, | |||
| 210 | + | C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, | |||
| 211 | + | nil, nil) // nil = main world | |||
| 212 | + | ||||
| 213 | + | if keyboardBlockerScript == nil { | |||
| 214 | + | return ErrNotImplemented | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | C.webkit_user_content_manager_add_script(w.native.ucm, keyboardBlockerScript) | |||
| 218 | + | log.Printf("[webkit] Page keyboard event blocking enabled") | |||
| 219 | + | return nil | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | // DisablePageKeyboardBlocking removes the keyboard blocking script and restores normal page behavior | |||
| 223 | + | func (w *WebView) DisablePageKeyboardBlocking() error { | |||
| 224 | + | if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil { | |||
| 225 | + | return ErrWebViewNotInitialized | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | // Execute cleanup script to remove event listeners | |||
| 229 | + | cleanupScript := ` | |||
| 230 | + | if (window.__dumber_cleanup_event_blocker) { | |||
| 231 | + | window.__dumber_cleanup_event_blocker(); | |||
| 232 | + | console.log('[dumber] Page keyboard event blocking disabled'); | |||
| 233 | + | } | |||
| 234 | + | ` | |||
| 235 | + | w.InjectScript(cleanupScript) | |||
| 236 | + | ||||
| 237 | + | // Remove the user script if it exists | |||
| 238 | + | if keyboardBlockerScript != nil { | |||
| 239 | + | C.webkit_user_content_manager_remove_script(w.native.ucm, keyboardBlockerScript) | |||
| 240 | + | C.webkit_user_script_unref(keyboardBlockerScript) | |||
| 241 | + | keyboardBlockerScript = nil | |||
| 242 | + | log.Printf("[webkit] Keyboard blocker script removed") | |||
| 243 | + | } | |||
| 244 | + | ||||
| 245 | + | return nil | |||
| 246 | + | } | |||
M pkg/webkit/webview_cgo.go +1 -0
| @@ -2580,6 +2580,7 @@ func (w *WebView) ID() string { | |||
| 2580 | 2580 | return strconv.FormatUint(uint64(w.id), 10) | |
| 2581 | 2581 | } | |
| 2582 | 2582 | ||
| 2583 | + | ||
| 2583 | 2584 | // PrefersDarkTheme returns true if GTK is configured to prefer dark theme | |
| 2584 | 2585 | func PrefersDarkTheme() bool { | |
| 2585 | 2586 | return C.gtk_prefers_dark() != 0 | |
| @@ -2580,6 +2580,7 @@ func (w *WebView) ID() string { | |||||
| 2580 | return strconv.FormatUint(uint64(w.id), 10) | 2580 | return strconv.FormatUint(uint64(w.id), 10) | ||
| 2581 | } | 2581 | } | ||
| 2582 | 2582 | ||||
| 2583 | + | ||||
| 2583 | // PrefersDarkTheme returns true if GTK is configured to prefer dark theme | 2584 | // PrefersDarkTheme returns true if GTK is configured to prefer dark theme | ||
| 2584 | func PrefersDarkTheme() bool { | 2585 | func PrefersDarkTheme() bool { | ||
| 2585 | return C.gtk_prefers_dark() != 0 | 2586 | return C.gtk_prefers_dark() != 0 | ||