fix(shortcuts): implement C-level to GTK4 bridge for Alt+Arrow workspace navigation
- commit
- 76cb885171039d98adf2a7af2f752b953f20b093
- author
- bamen · · 2025-09-22 18:39 UTC
- committer
- bamen · · 2025-09-22 21:58 UTC
- parent
- 549d2ce
- signature
- unsigned
8 files · +293 -71
M CHANGELOG.md +1 -0
| @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. | |||
| 8 | 8 | - **Print functionality**: Added Ctrl+Shift+P shortcut to open native print dialogs from any pane. Ctrl+P is blocked at WebKit level to prevent conflicts with pane mode shortcuts | |
| 9 | 9 | ||
| 10 | 10 | ### Fixed | |
| 11 | + | - **Alt+Arrow workspace navigation**: Fixed Alt+Arrow keys not working for pane navigation after WebView focus changes. Implemented C-level to GTK4 shortcut bridge that ensures workspace navigation works consistently across all panes | |
| 11 | 12 | - **Global shortcuts protection**: Fixed window shortcuts (Ctrl+L, Ctrl+F, F12) not working in new panes due to overly aggressive webpage protection. Window shortcuts now properly bubble up to GTK while pane shortcuts remain blocked from webpages. Resolves omnibox and developer tools not responding to keyboard shortcuts in newly created panes | |
| 12 | 13 | - **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 | |
| 13 | 14 | - **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 | |
| @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. | |||||
| 8 | - **Print functionality**: Added Ctrl+Shift+P shortcut to open native print dialogs from any pane. Ctrl+P is blocked at WebKit level to prevent conflicts with pane mode shortcuts | 8 | - **Print functionality**: Added Ctrl+Shift+P shortcut to open native print dialogs from any pane. Ctrl+P is blocked at WebKit level to prevent conflicts with pane mode shortcuts | ||
| 9 | 9 | ||||
| 10 | ### Fixed | 10 | ### Fixed | ||
| 11 | + | - **Alt+Arrow workspace navigation**: Fixed Alt+Arrow keys not working for pane navigation after WebView focus changes. Implemented C-level to GTK4 shortcut bridge that ensures workspace navigation works consistently across all panes | |||
| 11 | - **Global shortcuts protection**: Fixed window shortcuts (Ctrl+L, Ctrl+F, F12) not working in new panes due to overly aggressive webpage protection. Window shortcuts now properly bubble up to GTK while pane shortcuts remain blocked from webpages. Resolves omnibox and developer tools not responding to keyboard shortcuts in newly created panes | 12 | - **Global shortcuts protection**: Fixed window shortcuts (Ctrl+L, Ctrl+F, F12) not working in new panes due to overly aggressive webpage protection. Window shortcuts now properly bubble up to GTK while pane shortcuts remain blocked from webpages. Resolves omnibox and developer tools not responding to keyboard shortcuts in newly created panes | ||
| 12 | - **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 | 13 | - **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 | ||
| 13 | - **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 | 14 | - **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 | ||
M internal/app/browser/window_shortcuts.go +20 -0
| @@ -65,6 +65,11 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||
| 65 | 65 | {"ctrl+equal", h.handleZoomIn, "Zoom in (=)"}, | |
| 66 | 66 | {"ctrl+minus", h.handleZoomOut, "Zoom out"}, | |
| 67 | 67 | {"ctrl+0", h.handleZoomReset, "Zoom reset"}, | |
| 68 | + | // Workspace navigation shortcuts - global level for proper active pane targeting | |
| 69 | + | {"alt+ArrowLeft", func() { h.handleWorkspaceNavigation("left") }, "Navigate left pane"}, | |
| 70 | + | {"alt+ArrowRight", func() { h.handleWorkspaceNavigation("right") }, "Navigate right pane"}, | |
| 71 | + | {"alt+ArrowUp", func() { h.handleWorkspaceNavigation("up") }, "Navigate up pane"}, | |
| 72 | + | {"alt+ArrowDown", func() { h.handleWorkspaceNavigation("down") }, "Navigate down pane"}, | |
| 68 | 73 | } | |
| 69 | 74 | ||
| 70 | 75 | for _, shortcut := range shortcuts { | |
| @@ -286,6 +291,21 @@ func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureNam | |||
| 286 | 291 | } | |
| 287 | 292 | } | |
| 288 | 293 | ||
| 294 | + | func (h *WindowShortcutHandler) handleWorkspaceNavigation(direction string) { | |
| 295 | + | if h.app == nil || h.app.workspace == nil { | |
| 296 | + | log.Printf("[window-shortcuts] No workspace for navigation") | |
| 297 | + | return | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | log.Printf("[window-shortcuts] Workspace navigation: %s", direction) | |
| 301 | + | ||
| 302 | + | if h.app.workspace.FocusNeighbor(direction) { | |
| 303 | + | log.Printf("[window-shortcuts] Workspace navigation %s successful", direction) | |
| 304 | + | } else { | |
| 305 | + | log.Printf("[window-shortcuts] Workspace navigation %s failed", direction) | |
| 306 | + | } | |
| 307 | + | } | |
| 308 | + | ||
| 289 | 309 | // Cleanup releases resources | |
| 290 | 310 | func (h *WindowShortcutHandler) Cleanup() { | |
| 291 | 311 | if h.shortcuts != nil { | |
| @@ -65,6 +65,11 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error { | |||||
| 65 | {"ctrl+equal", h.handleZoomIn, "Zoom in (=)"}, | 65 | {"ctrl+equal", h.handleZoomIn, "Zoom in (=)"}, | ||
| 66 | {"ctrl+minus", h.handleZoomOut, "Zoom out"}, | 66 | {"ctrl+minus", h.handleZoomOut, "Zoom out"}, | ||
| 67 | {"ctrl+0", h.handleZoomReset, "Zoom reset"}, | 67 | {"ctrl+0", h.handleZoomReset, "Zoom reset"}, | ||
| 68 | + | // Workspace navigation shortcuts - global level for proper active pane targeting | |||
| 69 | + | {"alt+ArrowLeft", func() { h.handleWorkspaceNavigation("left") }, "Navigate left pane"}, | |||
| 70 | + | {"alt+ArrowRight", func() { h.handleWorkspaceNavigation("right") }, "Navigate right pane"}, | |||
| 71 | + | {"alt+ArrowUp", func() { h.handleWorkspaceNavigation("up") }, "Navigate up pane"}, | |||
| 72 | + | {"alt+ArrowDown", func() { h.handleWorkspaceNavigation("down") }, "Navigate down pane"}, | |||
| 68 | } | 73 | } | ||
| 69 | 74 | ||||
| 70 | for _, shortcut := range shortcuts { | 75 | for _, shortcut := range shortcuts { | ||
| @@ -286,6 +291,21 @@ func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureNam | |||||
| 286 | } | 291 | } | ||
| 287 | } | 292 | } | ||
| 288 | 293 | ||||
| 294 | + | func (h *WindowShortcutHandler) handleWorkspaceNavigation(direction string) { | |||
| 295 | + | if h.app == nil || h.app.workspace == nil { | |||
| 296 | + | log.Printf("[window-shortcuts] No workspace for navigation") | |||
| 297 | + | return | |||
| 298 | + | } | |||
| 299 | + | ||||
| 300 | + | log.Printf("[window-shortcuts] Workspace navigation: %s", direction) | |||
| 301 | + | ||||
| 302 | + | if h.app.workspace.FocusNeighbor(direction) { | |||
| 303 | + | log.Printf("[window-shortcuts] Workspace navigation %s successful", direction) | |||
| 304 | + | } else { | |||
| 305 | + | log.Printf("[window-shortcuts] Workspace navigation %s failed", direction) | |||
| 306 | + | } | |||
| 307 | + | } | |||
| 308 | + | ||||
| 289 | // Cleanup releases resources | 309 | // Cleanup releases resources | ||
| 290 | func (h *WindowShortcutHandler) Cleanup() { | 310 | func (h *WindowShortcutHandler) Cleanup() { | ||
| 291 | if h.shortcuts != nil { | 311 | if h.shortcuts != nil { | ||
M internal/app/browser/workspace_manager.go +4 -53
| @@ -67,52 +67,7 @@ const ( | |||
| 67 | 67 | multiPaneClass = "workspace-multi-pane" | |
| 68 | 68 | ) | |
| 69 | 69 | ||
| 70 | - | // registerWorkspaceShortcuts registers global workspace navigation shortcuts on the given webView. | |
| 71 | - | func (wm *WorkspaceManager) registerWorkspaceShortcuts(webView *webkit.WebView) { | |
| 72 | - | if wm == nil || webView == nil { | |
| 73 | - | return | |
| 74 | - | } | |
| 75 | - | ||
| 76 | - | registerFocusMove := func(key, dir string) { | |
| 77 | - | _ = webView.RegisterKeyboardShortcut(key, func() { | |
| 78 | - | log.Printf("[workspace] %s callback triggered on webView=%p", key, webView) | |
| 79 | - | if wm.app == nil { | |
| 80 | - | log.Printf("[workspace] %s rejected: app is nil", key) | |
| 81 | - | return | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | // Only allow the active webview to handle workspace navigation shortcuts | |
| 85 | - | if wm.app.activePane == nil || wm.app.activePane.webView != webView { | |
| 86 | - | log.Printf("[workspace] %s rejected: not active pane (active=%p, caller=%p)", | |
| 87 | - | key, wm.app.activePane.webView, webView) | |
| 88 | - | return | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | log.Printf("[workspace] %s calling FocusNeighbor(%s)", key, dir) | |
| 92 | - | if wm.FocusNeighbor(dir) { | |
| 93 | - | log.Printf("Shortcut: focus pane %s", dir) | |
| 94 | - | } else { | |
| 95 | - | log.Printf("[workspace] FocusNeighbor(%s) returned false", dir) | |
| 96 | - | } | |
| 97 | - | }) | |
| 98 | - | } | |
| 99 | - | ||
| 100 | - | // Pane navigation shortcuts | |
| 101 | - | registerFocusMove("alt+ArrowLeft", "left") | |
| 102 | - | registerFocusMove("alt+ArrowRight", "right") | |
| 103 | - | registerFocusMove("alt+ArrowUp", "up") | |
| 104 | - | registerFocusMove("alt+ArrowDown", "down") | |
| 105 | - | registerFocusMove("cmdorctrl+ArrowUp", "up") | |
| 106 | - | registerFocusMove("cmdorctrl+ArrowDown", "down") | |
| 107 | - | ||
| 108 | - | // Zoom shortcuts are now handled by global window shortcuts in window_shortcuts.go | |
| 109 | - | // for consistency with other global shortcuts like Ctrl+L, Ctrl+F, etc. | |
| 110 | - | ||
| 111 | - | // Reduced logging: only log shortcuts registration during initialization, not on every hover | |
| 112 | - | if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug { | |
| 113 | - | log.Printf("[workspace] registered navigation shortcuts on webView=%p", webView) | |
| 114 | - | } | |
| 115 | - | } | |
| 70 | + | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler | |
| 116 | 71 | ||
| 117 | 72 | // NewWorkspaceManager builds a workspace manager rooted at the provided pane. | |
| 118 | 73 | func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManager { | |
| @@ -163,7 +118,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||
| 163 | 118 | ||
| 164 | 119 | app.workspace = manager | |
| 165 | 120 | manager.focusNode(root) | |
| 166 | - | manager.registerWorkspaceShortcuts(root.pane.webView) | |
| 121 | + | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler | |
| 167 | 122 | // Ensure initial CSS classes are applied | |
| 168 | 123 | manager.ensurePaneBaseClasses() | |
| 169 | 124 | return manager | |
| @@ -433,8 +388,7 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) { | |||
| 433 | 388 | node.pane.webView.SetActive(true) | |
| 434 | 389 | } | |
| 435 | 390 | ||
| 436 | - | // Re-register workspace navigation shortcuts on the newly focused webView | |
| 437 | - | wm.registerWorkspaceShortcuts(node.pane.webView) | |
| 391 | + | // Shortcuts are registered once during webView creation, no need to re-register on focus | |
| 438 | 392 | ||
| 439 | 393 | if handler := node.pane.messageHandler; handler != nil { | |
| 440 | 394 | handler.SetWorkspaceObserver(wm) | |
| @@ -1514,10 +1468,7 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||
| 1514 | 1468 | return nil | |
| 1515 | 1469 | } | |
| 1516 | 1470 | ||
| 1517 | - | // Register workspace navigation shortcuts on popup WebView before creating pane | |
| 1518 | - | // This ensures shortcuts are available immediately when the WebView is ready | |
| 1519 | - | wm.registerWorkspaceShortcuts(newView) | |
| 1520 | - | log.Printf("[workspace] Registered workspace shortcuts for new WebView") | |
| 1471 | + | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler | |
| 1521 | 1472 | ||
| 1522 | 1473 | // Create a pane for the new WebView | |
| 1523 | 1474 | newPane, err := wm.createPane(newView) | |
| @@ -67,52 +67,7 @@ const ( | |||||
| 67 | multiPaneClass = "workspace-multi-pane" | 67 | multiPaneClass = "workspace-multi-pane" | ||
| 68 | ) | 68 | ) | ||
| 69 | 69 | ||||
| 70 | - | // registerWorkspaceShortcuts registers global workspace navigation shortcuts on the given webView. | 70 | + | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler |
| 71 | - | func (wm *WorkspaceManager) registerWorkspaceShortcuts(webView *webkit.WebView) { | |||
| 72 | - | if wm == nil || webView == nil { | |||
| 73 | - | return | |||
| 74 | - | } | |||
| 75 | - | ||||
| 76 | - | registerFocusMove := func(key, dir string) { | |||
| 77 | - | _ = webView.RegisterKeyboardShortcut(key, func() { | |||
| 78 | - | log.Printf("[workspace] %s callback triggered on webView=%p", key, webView) | |||
| 79 | - | if wm.app == nil { | |||
| 80 | - | log.Printf("[workspace] %s rejected: app is nil", key) | |||
| 81 | - | return | |||
| 82 | - | } | |||
| 83 | - | ||||
| 84 | - | // Only allow the active webview to handle workspace navigation shortcuts | |||
| 85 | - | if wm.app.activePane == nil || wm.app.activePane.webView != webView { | |||
| 86 | - | log.Printf("[workspace] %s rejected: not active pane (active=%p, caller=%p)", | |||
| 87 | - | key, wm.app.activePane.webView, webView) | |||
| 88 | - | return | |||
| 89 | - | } | |||
| 90 | - | ||||
| 91 | - | log.Printf("[workspace] %s calling FocusNeighbor(%s)", key, dir) | |||
| 92 | - | if wm.FocusNeighbor(dir) { | |||
| 93 | - | log.Printf("Shortcut: focus pane %s", dir) | |||
| 94 | - | } else { | |||
| 95 | - | log.Printf("[workspace] FocusNeighbor(%s) returned false", dir) | |||
| 96 | - | } | |||
| 97 | - | }) | |||
| 98 | - | } | |||
| 99 | - | ||||
| 100 | - | // Pane navigation shortcuts | |||
| 101 | - | registerFocusMove("alt+ArrowLeft", "left") | |||
| 102 | - | registerFocusMove("alt+ArrowRight", "right") | |||
| 103 | - | registerFocusMove("alt+ArrowUp", "up") | |||
| 104 | - | registerFocusMove("alt+ArrowDown", "down") | |||
| 105 | - | registerFocusMove("cmdorctrl+ArrowUp", "up") | |||
| 106 | - | registerFocusMove("cmdorctrl+ArrowDown", "down") | |||
| 107 | - | ||||
| 108 | - | // Zoom shortcuts are now handled by global window shortcuts in window_shortcuts.go | |||
| 109 | - | // for consistency with other global shortcuts like Ctrl+L, Ctrl+F, etc. | |||
| 110 | - | ||||
| 111 | - | // Reduced logging: only log shortcuts registration during initialization, not on every hover | |||
| 112 | - | if wm.app.config != nil && wm.app.config.Debug.EnableWorkspaceDebug { | |||
| 113 | - | log.Printf("[workspace] registered navigation shortcuts on webView=%p", webView) | |||
| 114 | - | } | |||
| 115 | - | } | |||
| 116 | 71 | ||||
| 117 | // NewWorkspaceManager builds a workspace manager rooted at the provided pane. | 72 | // NewWorkspaceManager builds a workspace manager rooted at the provided pane. | ||
| 118 | func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManager { | 73 | func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManager { | ||
| @@ -163,7 +118,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag | |||||
| 163 | 118 | ||||
| 164 | app.workspace = manager | 119 | app.workspace = manager | ||
| 165 | manager.focusNode(root) | 120 | manager.focusNode(root) | ||
| 166 | - | manager.registerWorkspaceShortcuts(root.pane.webView) | 121 | + | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler |
| 167 | // Ensure initial CSS classes are applied | 122 | // Ensure initial CSS classes are applied | ||
| 168 | manager.ensurePaneBaseClasses() | 123 | manager.ensurePaneBaseClasses() | ||
| 169 | return manager | 124 | return manager | ||
| @@ -433,8 +388,7 @@ func (wm *WorkspaceManager) focusNode(node *paneNode) { | |||||
| 433 | node.pane.webView.SetActive(true) | 388 | node.pane.webView.SetActive(true) | ||
| 434 | } | 389 | } | ||
| 435 | 390 | ||||
| 436 | - | // Re-register workspace navigation shortcuts on the newly focused webView | 391 | + | // Shortcuts are registered once during webView creation, no need to re-register on focus |
| 437 | - | wm.registerWorkspaceShortcuts(node.pane.webView) | |||
| 438 | 392 | ||||
| 439 | if handler := node.pane.messageHandler; handler != nil { | 393 | if handler := node.pane.messageHandler; handler != nil { | ||
| 440 | handler.SetWorkspaceObserver(wm) | 394 | handler.SetWorkspaceObserver(wm) | ||
| @@ -1514,10 +1468,7 @@ func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *web | |||||
| 1514 | return nil | 1468 | return nil | ||
| 1515 | } | 1469 | } | ||
| 1516 | 1470 | ||||
| 1517 | - | // Register workspace navigation shortcuts on popup WebView before creating pane | 1471 | + | // Workspace navigation shortcuts are now handled globally by WindowShortcutHandler |
| 1518 | - | // This ensures shortcuts are available immediately when the WebView is ready | |||
| 1519 | - | wm.registerWorkspaceShortcuts(newView) | |||
| 1520 | - | log.Printf("[workspace] Registered workspace shortcuts for new WebView") | |||
| 1521 | 1472 | ||||
| 1522 | // Create a pane for the new WebView | 1473 | // Create a pane for the new WebView | ||
| 1523 | newPane, err := wm.createPane(newView) | 1474 | newPane, err := wm.createPane(newView) | ||
M internal/app/browser/workspace_manager_test.go +78 -0
| @@ -191,3 +191,81 @@ func TestFocusNeighborWithoutPeer(t *testing.T) { | |||
| 191 | 191 | t.Fatalf("expected focus move to fail when only one pane exists") | |
| 192 | 192 | } | |
| 193 | 193 | } | |
| 194 | + | ||
| 195 | + | // TestWorkspaceNavigationAfterFocusChanges tests that workspace navigation | |
| 196 | + | // works correctly after focus changes between panes. This validates that | |
| 197 | + | // the FocusNeighbor functionality works regardless of which pane previously | |
| 198 | + | // had focus (since Alt+Arrow shortcuts are now handled globally). | |
| 199 | + | func TestWorkspaceNavigationAfterFocusChanges(t *testing.T) { | |
| 200 | + | wm := newTestWorkspaceManager(t) | |
| 201 | + | ||
| 202 | + | // Split to create two panes | |
| 203 | + | right, err := wm.splitNode(wm.root, "right") | |
| 204 | + | if err != nil { | |
| 205 | + | t.Fatalf("split failed: %v", err) | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | left := right.parent.left | |
| 209 | + | ||
| 210 | + | // Set up geometry for navigation calculations | |
| 211 | + | webkit.SetWidgetBoundsForTesting(left.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |
| 212 | + | webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100}) | |
| 213 | + | ||
| 214 | + | // Initially, the active pane should be the right one (newly created) | |
| 215 | + | if wm.GetActiveNode() != right { | |
| 216 | + | t.Fatalf("expected right pane to be active initially") | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | // Test that navigation works from right to left | |
| 220 | + | if !wm.FocusNeighbor("left") { | |
| 221 | + | t.Fatalf("expected focus neighbor left to succeed from right pane") | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | if wm.GetActiveNode() != left { | |
| 225 | + | t.Fatalf("expected focus to move to left pane") | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | // Test that navigation works from left to right | |
| 229 | + | if !wm.FocusNeighbor("right") { | |
| 230 | + | t.Fatalf("expected focus neighbor right to succeed from left pane") | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | if wm.GetActiveNode() != right { | |
| 234 | + | t.Fatalf("expected focus to move back to right pane") | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // Simulate multiple focus changes to verify that workspace navigation | |
| 238 | + | // works consistently (the old bug was per-webview shortcut registration) | |
| 239 | + | for i := 0; i < 3; i++ { | |
| 240 | + | // Move focus left | |
| 241 | + | wm.focusNode(left) | |
| 242 | + | if wm.GetActiveNode() != left { | |
| 243 | + | t.Fatalf("expected left pane to be active after focusNode call %d", i) | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | // Test navigation still works | |
| 247 | + | if !wm.FocusNeighbor("right") { | |
| 248 | + | t.Fatalf("expected navigation right to work after focus change %d", i) | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | // Move focus right | |
| 252 | + | wm.focusNode(right) | |
| 253 | + | if wm.GetActiveNode() != right { | |
| 254 | + | t.Fatalf("expected right pane to be active after focusNode call %d", i) | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | // Test navigation still works | |
| 258 | + | if !wm.FocusNeighbor("left") { | |
| 259 | + | t.Fatalf("expected navigation left to work after focus change %d", i) | |
| 260 | + | } | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | // Final verification that we can navigate in both directions | |
| 264 | + | if !wm.FocusNeighbor("right") { | |
| 265 | + | t.Fatalf("expected final navigation right to work") | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | if wm.GetActiveNode() != right { | |
| 269 | + | t.Fatalf("expected final active pane to be right") | |
| 270 | + | } | |
| 271 | + | } | |
| @@ -191,3 +191,81 @@ func TestFocusNeighborWithoutPeer(t *testing.T) { | |||||
| 191 | t.Fatalf("expected focus move to fail when only one pane exists") | 191 | t.Fatalf("expected focus move to fail when only one pane exists") | ||
| 192 | } | 192 | } | ||
| 193 | } | 193 | } | ||
| 194 | + | ||||
| 195 | + | // TestWorkspaceNavigationAfterFocusChanges tests that workspace navigation | |||
| 196 | + | // works correctly after focus changes between panes. This validates that | |||
| 197 | + | // the FocusNeighbor functionality works regardless of which pane previously | |||
| 198 | + | // had focus (since Alt+Arrow shortcuts are now handled globally). | |||
| 199 | + | func TestWorkspaceNavigationAfterFocusChanges(t *testing.T) { | |||
| 200 | + | wm := newTestWorkspaceManager(t) | |||
| 201 | + | ||||
| 202 | + | // Split to create two panes | |||
| 203 | + | right, err := wm.splitNode(wm.root, "right") | |||
| 204 | + | if err != nil { | |||
| 205 | + | t.Fatalf("split failed: %v", err) | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | left := right.parent.left | |||
| 209 | + | ||||
| 210 | + | // Set up geometry for navigation calculations | |||
| 211 | + | webkit.SetWidgetBoundsForTesting(left.container, webkit.WidgetBounds{X: 0, Y: 0, Width: 100, Height: 100}) | |||
| 212 | + | webkit.SetWidgetBoundsForTesting(right.container, webkit.WidgetBounds{X: 120, Y: 0, Width: 100, Height: 100}) | |||
| 213 | + | ||||
| 214 | + | // Initially, the active pane should be the right one (newly created) | |||
| 215 | + | if wm.GetActiveNode() != right { | |||
| 216 | + | t.Fatalf("expected right pane to be active initially") | |||
| 217 | + | } | |||
| 218 | + | ||||
| 219 | + | // Test that navigation works from right to left | |||
| 220 | + | if !wm.FocusNeighbor("left") { | |||
| 221 | + | t.Fatalf("expected focus neighbor left to succeed from right pane") | |||
| 222 | + | } | |||
| 223 | + | ||||
| 224 | + | if wm.GetActiveNode() != left { | |||
| 225 | + | t.Fatalf("expected focus to move to left pane") | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | // Test that navigation works from left to right | |||
| 229 | + | if !wm.FocusNeighbor("right") { | |||
| 230 | + | t.Fatalf("expected focus neighbor right to succeed from left pane") | |||
| 231 | + | } | |||
| 232 | + | ||||
| 233 | + | if wm.GetActiveNode() != right { | |||
| 234 | + | t.Fatalf("expected focus to move back to right pane") | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | // Simulate multiple focus changes to verify that workspace navigation | |||
| 238 | + | // works consistently (the old bug was per-webview shortcut registration) | |||
| 239 | + | for i := 0; i < 3; i++ { | |||
| 240 | + | // Move focus left | |||
| 241 | + | wm.focusNode(left) | |||
| 242 | + | if wm.GetActiveNode() != left { | |||
| 243 | + | t.Fatalf("expected left pane to be active after focusNode call %d", i) | |||
| 244 | + | } | |||
| 245 | + | ||||
| 246 | + | // Test navigation still works | |||
| 247 | + | if !wm.FocusNeighbor("right") { | |||
| 248 | + | t.Fatalf("expected navigation right to work after focus change %d", i) | |||
| 249 | + | } | |||
| 250 | + | ||||
| 251 | + | // Move focus right | |||
| 252 | + | wm.focusNode(right) | |||
| 253 | + | if wm.GetActiveNode() != right { | |||
| 254 | + | t.Fatalf("expected right pane to be active after focusNode call %d", i) | |||
| 255 | + | } | |||
| 256 | + | ||||
| 257 | + | // Test navigation still works | |||
| 258 | + | if !wm.FocusNeighbor("left") { | |||
| 259 | + | t.Fatalf("expected navigation left to work after focus change %d", i) | |||
| 260 | + | } | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | // Final verification that we can navigate in both directions | |||
| 264 | + | if !wm.FocusNeighbor("right") { | |||
| 265 | + | t.Fatalf("expected final navigation right to work") | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | if wm.GetActiveNode() != right { | |||
| 269 | + | t.Fatalf("expected final active pane to be right") | |||
| 270 | + | } | |||
| 271 | + | } | |||
M pkg/webkit/keyboard_cgo.go +63 -0
| @@ -146,6 +146,7 @@ func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) C.gboolea | |||
| 146 | 146 | uid := uintptr(id) | |
| 147 | 147 | kv := uint(keyval) | |
| 148 | 148 | st := uint(state) | |
| 149 | + | ||
| 149 | 150 | if dispatchAccelerator(uid, kv, st) { | |
| 150 | 151 | return C.gboolean(1) | |
| 151 | 152 | } | |
| @@ -219,6 +220,11 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool { | |||
| 219 | 220 | return false | |
| 220 | 221 | } | |
| 221 | 222 | ||
| 223 | + | // GTK-level fallback blocking for global shortcuts | |
| 224 | + | if checkGlobalShortcutBlocking(keyName, ctrl, alt, shift) { | |
| 225 | + | return true // Block the event from reaching WebKit | |
| 226 | + | } | |
| 227 | + | ||
| 222 | 228 | now := time.Now() | |
| 223 | 229 | rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName) | |
| 224 | 230 | if shouldDebounceShortcut(uid, rawKey, now) { | |
| @@ -275,6 +281,7 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool { | |||
| 275 | 281 | if reg == nil { | |
| 276 | 282 | return handled | |
| 277 | 283 | } | |
| 284 | + | ||
| 278 | 285 | for _, name := range candidates { | |
| 279 | 286 | if cb, ok := reg[name]; ok { | |
| 280 | 287 | if shouldDebounceShortcut(uid, "shortcut:"+name, now) { | |
| @@ -290,6 +297,62 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool { | |||
| 290 | 297 | return handled | |
| 291 | 298 | } | |
| 292 | 299 | ||
| 300 | + | // checkGlobalShortcutBlocking checks if a keyboard event should be blocked at GTK level | |
| 301 | + | // This provides defense-in-depth protection when JavaScript-level blocking fails | |
| 302 | + | func checkGlobalShortcutBlocking(keyName string, ctrl, alt, shift bool) bool { | |
| 303 | + | if keyName == "" { | |
| 304 | + | return false | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | // Build shortcut string similar to the JavaScript protection logic | |
| 308 | + | var shortcut strings.Builder | |
| 309 | + | if ctrl { | |
| 310 | + | shortcut.WriteString("ctrl+") | |
| 311 | + | } | |
| 312 | + | if alt { | |
| 313 | + | shortcut.WriteString("alt+") | |
| 314 | + | } | |
| 315 | + | if shift { | |
| 316 | + | shortcut.WriteString("shift+") | |
| 317 | + | } | |
| 318 | + | shortcut.WriteString(strings.ToLower(keyName)) | |
| 319 | + | ||
| 320 | + | shortcutStr := shortcut.String() | |
| 321 | + | ||
| 322 | + | // Check if this shortcut is in the global registry | |
| 323 | + | shortcuts := GetRegisteredShortcuts() | |
| 324 | + | for _, registered := range shortcuts { | |
| 325 | + | if registered == shortcutStr { | |
| 326 | + | // Trigger the callback handle if available (especially for Alt+Arrow shortcuts) | |
| 327 | + | triggerGlobalShortcutCallback(registered) | |
| 328 | + | return true | |
| 329 | + | } | |
| 330 | + | // Also check common variations | |
| 331 | + | if registered == strings.ReplaceAll(shortcutStr, "arrowleft", "left") || | |
| 332 | + | registered == strings.ReplaceAll(shortcutStr, "arrowright", "right") || | |
| 333 | + | registered == strings.ReplaceAll(shortcutStr, "arrowup", "up") || | |
| 334 | + | registered == strings.ReplaceAll(shortcutStr, "arrowdown", "down") { | |
| 335 | + | // Trigger the callback handle for the registered variant | |
| 336 | + | triggerGlobalShortcutCallback(registered) | |
| 337 | + | return true | |
| 338 | + | } | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | return false | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | // triggerGlobalShortcutCallback looks up and invokes the callback handle for a given shortcut | |
| 345 | + | func triggerGlobalShortcutCallback(shortcut string) { | |
| 346 | + | // Access the global shortcut handles map from shortcuts_cgo.go | |
| 347 | + | shortcutsRegistryMu.RLock() | |
| 348 | + | handle, exists := globalShortcutHandles[shortcut] | |
| 349 | + | shortcutsRegistryMu.RUnlock() | |
| 350 | + | ||
| 351 | + | if exists && handle != 0 { | |
| 352 | + | invokeWindowShortcutCallback(handle) | |
| 353 | + | } | |
| 354 | + | } | |
| 355 | + | ||
| 293 | 356 | func nextViewID() uintptr { | |
| 294 | 357 | return uintptr(atomic.AddUint64(&viewIDCounter, 1)) | |
| 295 | 358 | } | |
| @@ -146,6 +146,7 @@ func goOnKeyPress(id C.ulong, keyval C.guint, state C.GdkModifierType) C.gboolea | |||||
| 146 | uid := uintptr(id) | 146 | uid := uintptr(id) | ||
| 147 | kv := uint(keyval) | 147 | kv := uint(keyval) | ||
| 148 | st := uint(state) | 148 | st := uint(state) | ||
| 149 | + | ||||
| 149 | if dispatchAccelerator(uid, kv, st) { | 150 | if dispatchAccelerator(uid, kv, st) { | ||
| 150 | return C.gboolean(1) | 151 | return C.gboolean(1) | ||
| 151 | } | 152 | } | ||
| @@ -219,6 +220,11 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool { | |||||
| 219 | return false | 220 | return false | ||
| 220 | } | 221 | } | ||
| 221 | 222 | ||||
| 223 | + | // GTK-level fallback blocking for global shortcuts | |||
| 224 | + | if checkGlobalShortcutBlocking(keyName, ctrl, alt, shift) { | |||
| 225 | + | return true // Block the event from reaching WebKit | |||
| 226 | + | } | |||
| 227 | + | ||||
| 222 | now := time.Now() | 228 | now := time.Now() | ||
| 223 | rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName) | 229 | rawKey := fmt.Sprintf("raw:%v:%v:%v:%s", ctrl, alt, shift, keyName) | ||
| 224 | if shouldDebounceShortcut(uid, rawKey, now) { | 230 | if shouldDebounceShortcut(uid, rawKey, now) { | ||
| @@ -275,6 +281,7 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool { | |||||
| 275 | if reg == nil { | 281 | if reg == nil { | ||
| 276 | return handled | 282 | return handled | ||
| 277 | } | 283 | } | ||
| 284 | + | ||||
| 278 | for _, name := range candidates { | 285 | for _, name := range candidates { | ||
| 279 | if cb, ok := reg[name]; ok { | 286 | if cb, ok := reg[name]; ok { | ||
| 280 | if shouldDebounceShortcut(uid, "shortcut:"+name, now) { | 287 | if shouldDebounceShortcut(uid, "shortcut:"+name, now) { | ||
| @@ -290,6 +297,62 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool { | |||||
| 290 | return handled | 297 | return handled | ||
| 291 | } | 298 | } | ||
| 292 | 299 | ||||
| 300 | + | // checkGlobalShortcutBlocking checks if a keyboard event should be blocked at GTK level | |||
| 301 | + | // This provides defense-in-depth protection when JavaScript-level blocking fails | |||
| 302 | + | func checkGlobalShortcutBlocking(keyName string, ctrl, alt, shift bool) bool { | |||
| 303 | + | if keyName == "" { | |||
| 304 | + | return false | |||
| 305 | + | } | |||
| 306 | + | ||||
| 307 | + | // Build shortcut string similar to the JavaScript protection logic | |||
| 308 | + | var shortcut strings.Builder | |||
| 309 | + | if ctrl { | |||
| 310 | + | shortcut.WriteString("ctrl+") | |||
| 311 | + | } | |||
| 312 | + | if alt { | |||
| 313 | + | shortcut.WriteString("alt+") | |||
| 314 | + | } | |||
| 315 | + | if shift { | |||
| 316 | + | shortcut.WriteString("shift+") | |||
| 317 | + | } | |||
| 318 | + | shortcut.WriteString(strings.ToLower(keyName)) | |||
| 319 | + | ||||
| 320 | + | shortcutStr := shortcut.String() | |||
| 321 | + | ||||
| 322 | + | // Check if this shortcut is in the global registry | |||
| 323 | + | shortcuts := GetRegisteredShortcuts() | |||
| 324 | + | for _, registered := range shortcuts { | |||
| 325 | + | if registered == shortcutStr { | |||
| 326 | + | // Trigger the callback handle if available (especially for Alt+Arrow shortcuts) | |||
| 327 | + | triggerGlobalShortcutCallback(registered) | |||
| 328 | + | return true | |||
| 329 | + | } | |||
| 330 | + | // Also check common variations | |||
| 331 | + | if registered == strings.ReplaceAll(shortcutStr, "arrowleft", "left") || | |||
| 332 | + | registered == strings.ReplaceAll(shortcutStr, "arrowright", "right") || | |||
| 333 | + | registered == strings.ReplaceAll(shortcutStr, "arrowup", "up") || | |||
| 334 | + | registered == strings.ReplaceAll(shortcutStr, "arrowdown", "down") { | |||
| 335 | + | // Trigger the callback handle for the registered variant | |||
| 336 | + | triggerGlobalShortcutCallback(registered) | |||
| 337 | + | return true | |||
| 338 | + | } | |||
| 339 | + | } | |||
| 340 | + | ||||
| 341 | + | return false | |||
| 342 | + | } | |||
| 343 | + | ||||
| 344 | + | // triggerGlobalShortcutCallback looks up and invokes the callback handle for a given shortcut | |||
| 345 | + | func triggerGlobalShortcutCallback(shortcut string) { | |||
| 346 | + | // Access the global shortcut handles map from shortcuts_cgo.go | |||
| 347 | + | shortcutsRegistryMu.RLock() | |||
| 348 | + | handle, exists := globalShortcutHandles[shortcut] | |||
| 349 | + | shortcutsRegistryMu.RUnlock() | |||
| 350 | + | ||||
| 351 | + | if exists && handle != 0 { | |||
| 352 | + | invokeWindowShortcutCallback(handle) | |||
| 353 | + | } | |||
| 354 | + | } | |||
| 355 | + | ||||
| 293 | func nextViewID() uintptr { | 356 | func nextViewID() uintptr { | ||
| 294 | return uintptr(atomic.AddUint64(&viewIDCounter, 1)) | 357 | return uintptr(atomic.AddUint64(&viewIDCounter, 1)) | ||
| 295 | } | 358 | } | ||
M pkg/webkit/shortcuts_cgo.go +113 -16
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | package webkit | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | + | "fmt" | |
| 6 | 7 | "log" | |
| 7 | 8 | "strings" | |
| 8 | 9 | "sync" | |
| @@ -149,21 +150,47 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err | |||
| 149 | 150 | var ( | |
| 150 | 151 | globalShortcutsRegistry = make(map[string]bool) | |
| 151 | 152 | shortcutsRegistryMu sync.RWMutex | |
| 153 | + | // Map from shortcut key to callback handle for C-level triggering | |
| 154 | + | globalShortcutHandles = make(map[string]uintptr) | |
| 155 | + | ) | |
| 156 | + | ||
| 157 | + | // WebView tracking for dynamic protection refresh | |
| 158 | + | var ( | |
| 159 | + | activeWebViews = make(map[*WebView]bool) | |
| 160 | + | activeWebViewsMu sync.RWMutex | |
| 152 | 161 | ) | |
| 153 | 162 | ||
| 154 | 163 | // RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking | |
| 155 | 164 | func RegisterGlobalShortcut(shortcut string) { | |
| 156 | 165 | shortcutsRegistryMu.Lock() | |
| 157 | - | defer shortcutsRegistryMu.Unlock() | |
| 158 | - | ||
| 159 | 166 | // Normalize shortcut format (convert to lowercase, standardize modifiers) | |
| 160 | 167 | normalized := normalizeShortcut(shortcut) | |
| 161 | 168 | ||
| 162 | - | // Only log if this is a new shortcut registration | |
| 163 | - | if !globalShortcutsRegistry[normalized] { | |
| 169 | + | // Only proceed if this is a new shortcut registration | |
| 170 | + | wasNew := !globalShortcutsRegistry[normalized] | |
| 171 | + | if wasNew { | |
| 164 | 172 | globalShortcutsRegistry[normalized] = true | |
| 165 | 173 | log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized) | |
| 166 | 174 | } | |
| 175 | + | shortcutsRegistryMu.Unlock() | |
| 176 | + | ||
| 177 | + | // If we added a new shortcut, refresh protection on all active WebViews | |
| 178 | + | if wasNew { | |
| 179 | + | RefreshGlobalShortcutsProtection() | |
| 180 | + | } | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | // RegisterGlobalShortcutWithHandle adds a shortcut with its callback handle for C-level triggering | |
| 184 | + | func RegisterGlobalShortcutWithHandle(shortcut string, handle uintptr) { | |
| 185 | + | // First register the shortcut for blocking | |
| 186 | + | RegisterGlobalShortcut(shortcut) | |
| 187 | + | ||
| 188 | + | // Then store the callback handle for C-level triggering | |
| 189 | + | shortcutsRegistryMu.Lock() | |
| 190 | + | normalized := normalizeShortcut(shortcut) | |
| 191 | + | globalShortcutHandles[normalized] = handle | |
| 192 | + | log.Printf("[shortcuts] Registered callback handle for shortcut: %s", shortcut) | |
| 193 | + | shortcutsRegistryMu.Unlock() | |
| 167 | 194 | } | |
| 168 | 195 | ||
| 169 | 196 | // GetRegisteredShortcuts returns a copy of all registered shortcuts | |
| @@ -178,6 +205,51 @@ func GetRegisteredShortcuts() []string { | |||
| 178 | 205 | return shortcuts | |
| 179 | 206 | } | |
| 180 | 207 | ||
| 208 | + | // RegisterActiveWebView adds a WebView to the active registry for dynamic protection updates | |
| 209 | + | func RegisterActiveWebView(w *WebView) { | |
| 210 | + | if w == nil { | |
| 211 | + | return | |
| 212 | + | } | |
| 213 | + | activeWebViewsMu.Lock() | |
| 214 | + | defer activeWebViewsMu.Unlock() | |
| 215 | + | activeWebViews[w] = true | |
| 216 | + | // WebView registered for protection updates | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | // UnregisterActiveWebView removes a WebView from the active registry | |
| 220 | + | func UnregisterActiveWebView(w *WebView) { | |
| 221 | + | if w == nil { | |
| 222 | + | return | |
| 223 | + | } | |
| 224 | + | activeWebViewsMu.Lock() | |
| 225 | + | defer activeWebViewsMu.Unlock() | |
| 226 | + | delete(activeWebViews, w) | |
| 227 | + | // WebView unregistered from protection updates | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | // RefreshGlobalShortcutsProtection updates all active WebViews with current shortcut protection | |
| 231 | + | func RefreshGlobalShortcutsProtection() { | |
| 232 | + | activeWebViewsMu.RLock() | |
| 233 | + | webViews := make([]*WebView, 0, len(activeWebViews)) | |
| 234 | + | for wv := range activeWebViews { | |
| 235 | + | webViews = append(webViews, wv) | |
| 236 | + | } | |
| 237 | + | activeWebViewsMu.RUnlock() | |
| 238 | + | ||
| 239 | + | if len(webViews) == 0 { | |
| 240 | + | return | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | for _, wv := range webViews { | |
| 244 | + | if wv != nil && !wv.destroyed { | |
| 245 | + | // Re-enable protection which will inject updated shortcut list | |
| 246 | + | if err := wv.EnableGlobalShortcutsProtection(); err != nil { | |
| 247 | + | log.Printf("[shortcuts] Failed to refresh protection for WebView: %v", err) | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | ||
| 181 | 253 | // normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking | |
| 182 | 254 | func normalizeShortcut(shortcut string) string { | |
| 183 | 255 | // Convert to lowercase and replace common variations | |
| @@ -188,11 +260,19 @@ func normalizeShortcut(shortcut string) string { | |||
| 188 | 260 | normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking | |
| 189 | 261 | normalized = strings.ReplaceAll(normalized, "control+", "ctrl+") | |
| 190 | 262 | ||
| 191 | - | // Standardize key names | |
| 192 | - | normalized = strings.ReplaceAll(normalized, "arrowleft", "arrowleft") | |
| 193 | - | normalized = strings.ReplaceAll(normalized, "arrowright", "arrowright") | |
| 194 | - | normalized = strings.ReplaceAll(normalized, "arrowup", "arrowup") | |
| 195 | - | normalized = strings.ReplaceAll(normalized, "arrowdown", "arrowdown") | |
| 263 | + | // Standardize arrow key names (handle both formats) | |
| 264 | + | normalized = strings.ReplaceAll(normalized, "left", "arrowleft") | |
| 265 | + | normalized = strings.ReplaceAll(normalized, "right", "arrowright") | |
| 266 | + | normalized = strings.ReplaceAll(normalized, "up", "arrowup") | |
| 267 | + | normalized = strings.ReplaceAll(normalized, "down", "arrowdown") | |
| 268 | + | ||
| 269 | + | // Fix double arrow prefixes that might occur from above replacements | |
| 270 | + | normalized = strings.ReplaceAll(normalized, "arrowarrowleft", "arrowleft") | |
| 271 | + | normalized = strings.ReplaceAll(normalized, "arrowarrowright", "arrowright") | |
| 272 | + | normalized = strings.ReplaceAll(normalized, "arrowarrowup", "arrowup") | |
| 273 | + | normalized = strings.ReplaceAll(normalized, "arrowarrowdown", "arrowdown") | |
| 274 | + | ||
| 275 | + | // Other key normalizations | |
| 196 | 276 | normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+= | |
| 197 | 277 | normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to = | |
| 198 | 278 | ||
| @@ -227,15 +307,25 @@ func (w *WebView) EnableGlobalShortcutsProtection() error { | |||
| 227 | 307 | } | |
| 228 | 308 | shortcutsJS += "]" | |
| 229 | 309 | ||
| 230 | - | // Create the blocking script that only blocks registered global shortcuts | |
| 310 | + | // Create a version-based blocking script that can be updated dynamically | |
| 311 | + | protectionVersion := len(shortcuts) // Use shortcuts count as version | |
| 231 | 312 | globalProtectionScript := ` | |
| 232 | 313 | (() => { | |
| 233 | - | if (!window.__dumber_global_shortcuts_protection) { | |
| 234 | - | window.__dumber_global_shortcuts_protection = true; | |
| 314 | + | const newVersion = ` + fmt.Sprintf("%d", protectionVersion) + `; | |
| 315 | + | const currentVersion = window.__dumber_shortcuts_protection_version || 0; | |
| 316 | + | ||
| 317 | + | // Only update if this is a newer version or first time | |
| 318 | + | if (newVersion > currentVersion) { | |
| 319 | + | // Clean up old protection if it exists | |
| 320 | + | if (window.__dumber_cleanup_global_shortcuts) { | |
| 321 | + | window.__dumber_cleanup_global_shortcuts(); | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | window.__dumber_shortcuts_protection_version = newVersion; | |
| 235 | 325 | ||
| 236 | 326 | // List of global shortcuts to block from reaching the webpage | |
| 237 | 327 | const globalShortcuts = ` + shortcutsJS + `; | |
| 238 | - | console.log('[dumber] Protecting global shortcuts:', globalShortcuts); | |
| 328 | + | console.log('[dumber] Protecting global shortcuts (v' + newVersion + '):', globalShortcuts); | |
| 239 | 329 | ||
| 240 | 330 | // Function to check if a key event matches any global shortcut | |
| 241 | 331 | const isGlobalShortcut = (e) => { | |
| @@ -260,7 +350,12 @@ func (w *WebView) EnableGlobalShortcutsProtection() error { | |||
| 260 | 350 | 'f12': ['f12'], | |
| 261 | 351 | 'ctrl+r': ['ctrl+r'], | |
| 262 | 352 | 'ctrl+shift+r': ['ctrl+shift+r'], | |
| 263 | - | 'f5': ['f5'] | |
| 353 | + | 'f5': ['f5'], | |
| 354 | + | // Alt+Arrow key mappings (handle both key and code variations) | |
| 355 | + | 'alt+arrowleft': ['alt+left', 'alt+arrowleft'], | |
| 356 | + | 'alt+arrowright': ['alt+right', 'alt+arrowright'], | |
| 357 | + | 'alt+arrowup': ['alt+up', 'alt+arrowup'], | |
| 358 | + | 'alt+arrowdown': ['alt+down', 'alt+arrowdown'] | |
| 264 | 359 | }; | |
| 265 | 360 | ||
| 266 | 361 | // Check direct matches | |
| @@ -302,12 +397,14 @@ func (w *WebView) EnableGlobalShortcutsProtection() error { | |||
| 302 | 397 | document.removeEventListener('keydown', blockGlobalShortcut, true); | |
| 303 | 398 | document.removeEventListener('keyup', blockGlobalShortcut, true); | |
| 304 | 399 | document.removeEventListener('keypress', blockGlobalShortcut, true); | |
| 305 | - | delete window.__dumber_global_shortcuts_protection; | |
| 400 | + | delete window.__dumber_shortcuts_protection_version; | |
| 306 | 401 | delete window.__dumber_cleanup_global_shortcuts; | |
| 307 | 402 | console.log('[dumber] Global shortcuts protection disabled'); | |
| 308 | 403 | }; | |
| 309 | 404 | ||
| 310 | - | console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts'); | |
| 405 | + | console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts (v' + newVersion + ')'); | |
| 406 | + | } else { | |
| 407 | + | console.log('[dumber] Global shortcuts protection already up to date (v' + currentVersion + ')'); | |
| 311 | 408 | } | |
| 312 | 409 | })(); | |
| 313 | 410 | ` | |
| @@ -3,6 +3,7 @@ | |||||
| 3 | package webkit | 3 | package webkit | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | + | "fmt" | |||
| 6 | "log" | 7 | "log" | ||
| 7 | "strings" | 8 | "strings" | ||
| 8 | "sync" | 9 | "sync" | ||
| @@ -149,21 +150,47 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err | |||||
| 149 | var ( | 150 | var ( | ||
| 150 | globalShortcutsRegistry = make(map[string]bool) | 151 | globalShortcutsRegistry = make(map[string]bool) | ||
| 151 | shortcutsRegistryMu sync.RWMutex | 152 | shortcutsRegistryMu sync.RWMutex | ||
| 153 | + | // Map from shortcut key to callback handle for C-level triggering | |||
| 154 | + | globalShortcutHandles = make(map[string]uintptr) | |||
| 155 | + | ) | |||
| 156 | + | ||||
| 157 | + | // WebView tracking for dynamic protection refresh | |||
| 158 | + | var ( | |||
| 159 | + | activeWebViews = make(map[*WebView]bool) | |||
| 160 | + | activeWebViewsMu sync.RWMutex | |||
| 152 | ) | 161 | ) | ||
| 153 | 162 | ||||
| 154 | // RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking | 163 | // RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking | ||
| 155 | func RegisterGlobalShortcut(shortcut string) { | 164 | func RegisterGlobalShortcut(shortcut string) { | ||
| 156 | shortcutsRegistryMu.Lock() | 165 | shortcutsRegistryMu.Lock() | ||
| 157 | - | defer shortcutsRegistryMu.Unlock() | |||
| 158 | - | ||||
| 159 | // Normalize shortcut format (convert to lowercase, standardize modifiers) | 166 | // Normalize shortcut format (convert to lowercase, standardize modifiers) | ||
| 160 | normalized := normalizeShortcut(shortcut) | 167 | normalized := normalizeShortcut(shortcut) | ||
| 161 | 168 | ||||
| 162 | - | // Only log if this is a new shortcut registration | 169 | + | // Only proceed if this is a new shortcut registration |
| 163 | - | if !globalShortcutsRegistry[normalized] { | 170 | + | wasNew := !globalShortcutsRegistry[normalized] |
| 171 | + | if wasNew { | |||
| 164 | globalShortcutsRegistry[normalized] = true | 172 | globalShortcutsRegistry[normalized] = true | ||
| 165 | log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized) | 173 | log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized) | ||
| 166 | } | 174 | } | ||
| 175 | + | shortcutsRegistryMu.Unlock() | |||
| 176 | + | ||||
| 177 | + | // If we added a new shortcut, refresh protection on all active WebViews | |||
| 178 | + | if wasNew { | |||
| 179 | + | RefreshGlobalShortcutsProtection() | |||
| 180 | + | } | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | // RegisterGlobalShortcutWithHandle adds a shortcut with its callback handle for C-level triggering | |||
| 184 | + | func RegisterGlobalShortcutWithHandle(shortcut string, handle uintptr) { | |||
| 185 | + | // First register the shortcut for blocking | |||
| 186 | + | RegisterGlobalShortcut(shortcut) | |||
| 187 | + | ||||
| 188 | + | // Then store the callback handle for C-level triggering | |||
| 189 | + | shortcutsRegistryMu.Lock() | |||
| 190 | + | normalized := normalizeShortcut(shortcut) | |||
| 191 | + | globalShortcutHandles[normalized] = handle | |||
| 192 | + | log.Printf("[shortcuts] Registered callback handle for shortcut: %s", shortcut) | |||
| 193 | + | shortcutsRegistryMu.Unlock() | |||
| 167 | } | 194 | } | ||
| 168 | 195 | ||||
| 169 | // GetRegisteredShortcuts returns a copy of all registered shortcuts | 196 | // GetRegisteredShortcuts returns a copy of all registered shortcuts | ||
| @@ -178,6 +205,51 @@ func GetRegisteredShortcuts() []string { | |||||
| 178 | return shortcuts | 205 | return shortcuts | ||
| 179 | } | 206 | } | ||
| 180 | 207 | ||||
| 208 | + | // RegisterActiveWebView adds a WebView to the active registry for dynamic protection updates | |||
| 209 | + | func RegisterActiveWebView(w *WebView) { | |||
| 210 | + | if w == nil { | |||
| 211 | + | return | |||
| 212 | + | } | |||
| 213 | + | activeWebViewsMu.Lock() | |||
| 214 | + | defer activeWebViewsMu.Unlock() | |||
| 215 | + | activeWebViews[w] = true | |||
| 216 | + | // WebView registered for protection updates | |||
| 217 | + | } | |||
| 218 | + | ||||
| 219 | + | // UnregisterActiveWebView removes a WebView from the active registry | |||
| 220 | + | func UnregisterActiveWebView(w *WebView) { | |||
| 221 | + | if w == nil { | |||
| 222 | + | return | |||
| 223 | + | } | |||
| 224 | + | activeWebViewsMu.Lock() | |||
| 225 | + | defer activeWebViewsMu.Unlock() | |||
| 226 | + | delete(activeWebViews, w) | |||
| 227 | + | // WebView unregistered from protection updates | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | // RefreshGlobalShortcutsProtection updates all active WebViews with current shortcut protection | |||
| 231 | + | func RefreshGlobalShortcutsProtection() { | |||
| 232 | + | activeWebViewsMu.RLock() | |||
| 233 | + | webViews := make([]*WebView, 0, len(activeWebViews)) | |||
| 234 | + | for wv := range activeWebViews { | |||
| 235 | + | webViews = append(webViews, wv) | |||
| 236 | + | } | |||
| 237 | + | activeWebViewsMu.RUnlock() | |||
| 238 | + | ||||
| 239 | + | if len(webViews) == 0 { | |||
| 240 | + | return | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | for _, wv := range webViews { | |||
| 244 | + | if wv != nil && !wv.destroyed { | |||
| 245 | + | // Re-enable protection which will inject updated shortcut list | |||
| 246 | + | if err := wv.EnableGlobalShortcutsProtection(); err != nil { | |||
| 247 | + | log.Printf("[shortcuts] Failed to refresh protection for WebView: %v", err) | |||
| 248 | + | } | |||
| 249 | + | } | |||
| 250 | + | } | |||
| 251 | + | } | |||
| 252 | + | ||||
| 181 | // normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking | 253 | // normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking | ||
| 182 | func normalizeShortcut(shortcut string) string { | 254 | func normalizeShortcut(shortcut string) string { | ||
| 183 | // Convert to lowercase and replace common variations | 255 | // Convert to lowercase and replace common variations | ||
| @@ -188,11 +260,19 @@ func normalizeShortcut(shortcut string) string { | |||||
| 188 | normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking | 260 | normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking | ||
| 189 | normalized = strings.ReplaceAll(normalized, "control+", "ctrl+") | 261 | normalized = strings.ReplaceAll(normalized, "control+", "ctrl+") | ||
| 190 | 262 | ||||
| 191 | - | // Standardize key names | 263 | + | // Standardize arrow key names (handle both formats) |
| 192 | - | normalized = strings.ReplaceAll(normalized, "arrowleft", "arrowleft") | 264 | + | normalized = strings.ReplaceAll(normalized, "left", "arrowleft") |
| 193 | - | normalized = strings.ReplaceAll(normalized, "arrowright", "arrowright") | 265 | + | normalized = strings.ReplaceAll(normalized, "right", "arrowright") |
| 194 | - | normalized = strings.ReplaceAll(normalized, "arrowup", "arrowup") | 266 | + | normalized = strings.ReplaceAll(normalized, "up", "arrowup") |
| 195 | - | normalized = strings.ReplaceAll(normalized, "arrowdown", "arrowdown") | 267 | + | normalized = strings.ReplaceAll(normalized, "down", "arrowdown") |
| 268 | + | ||||
| 269 | + | // Fix double arrow prefixes that might occur from above replacements | |||
| 270 | + | normalized = strings.ReplaceAll(normalized, "arrowarrowleft", "arrowleft") | |||
| 271 | + | normalized = strings.ReplaceAll(normalized, "arrowarrowright", "arrowright") | |||
| 272 | + | normalized = strings.ReplaceAll(normalized, "arrowarrowup", "arrowup") | |||
| 273 | + | normalized = strings.ReplaceAll(normalized, "arrowarrowdown", "arrowdown") | |||
| 274 | + | ||||
| 275 | + | // Other key normalizations | |||
| 196 | normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+= | 276 | normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+= | ||
| 197 | normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to = | 277 | normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to = | ||
| 198 | 278 | ||||
| @@ -227,15 +307,25 @@ func (w *WebView) EnableGlobalShortcutsProtection() error { | |||||
| 227 | } | 307 | } | ||
| 228 | shortcutsJS += "]" | 308 | shortcutsJS += "]" | ||
| 229 | 309 | ||||
| 230 | - | // Create the blocking script that only blocks registered global shortcuts | 310 | + | // Create a version-based blocking script that can be updated dynamically |
| 311 | + | protectionVersion := len(shortcuts) // Use shortcuts count as version | |||
| 231 | globalProtectionScript := ` | 312 | globalProtectionScript := ` | ||
| 232 | (() => { | 313 | (() => { | ||
| 233 | - | if (!window.__dumber_global_shortcuts_protection) { | 314 | + | const newVersion = ` + fmt.Sprintf("%d", protectionVersion) + `; |
| 234 | - | window.__dumber_global_shortcuts_protection = true; | 315 | + | const currentVersion = window.__dumber_shortcuts_protection_version || 0; |
| 316 | + | ||||
| 317 | + | // Only update if this is a newer version or first time | |||
| 318 | + | if (newVersion > currentVersion) { | |||
| 319 | + | // Clean up old protection if it exists | |||
| 320 | + | if (window.__dumber_cleanup_global_shortcuts) { | |||
| 321 | + | window.__dumber_cleanup_global_shortcuts(); | |||
| 322 | + | } | |||
| 323 | + | ||||
| 324 | + | window.__dumber_shortcuts_protection_version = newVersion; | |||
| 235 | 325 | ||||
| 236 | // List of global shortcuts to block from reaching the webpage | 326 | // List of global shortcuts to block from reaching the webpage | ||
| 237 | const globalShortcuts = ` + shortcutsJS + `; | 327 | const globalShortcuts = ` + shortcutsJS + `; | ||
| 238 | - | console.log('[dumber] Protecting global shortcuts:', globalShortcuts); | 328 | + | console.log('[dumber] Protecting global shortcuts (v' + newVersion + '):', globalShortcuts); |
| 239 | 329 | ||||
| 240 | // Function to check if a key event matches any global shortcut | 330 | // Function to check if a key event matches any global shortcut | ||
| 241 | const isGlobalShortcut = (e) => { | 331 | const isGlobalShortcut = (e) => { | ||
| @@ -260,7 +350,12 @@ func (w *WebView) EnableGlobalShortcutsProtection() error { | |||||
| 260 | 'f12': ['f12'], | 350 | 'f12': ['f12'], | ||
| 261 | 'ctrl+r': ['ctrl+r'], | 351 | 'ctrl+r': ['ctrl+r'], | ||
| 262 | 'ctrl+shift+r': ['ctrl+shift+r'], | 352 | 'ctrl+shift+r': ['ctrl+shift+r'], | ||
| 263 | - | 'f5': ['f5'] | 353 | + | 'f5': ['f5'], |
| 354 | + | // Alt+Arrow key mappings (handle both key and code variations) | |||
| 355 | + | 'alt+arrowleft': ['alt+left', 'alt+arrowleft'], | |||
| 356 | + | 'alt+arrowright': ['alt+right', 'alt+arrowright'], | |||
| 357 | + | 'alt+arrowup': ['alt+up', 'alt+arrowup'], | |||
| 358 | + | 'alt+arrowdown': ['alt+down', 'alt+arrowdown'] | |||
| 264 | }; | 359 | }; | ||
| 265 | 360 | ||||
| 266 | // Check direct matches | 361 | // Check direct matches | ||
| @@ -302,12 +397,14 @@ func (w *WebView) EnableGlobalShortcutsProtection() error { | |||||
| 302 | document.removeEventListener('keydown', blockGlobalShortcut, true); | 397 | document.removeEventListener('keydown', blockGlobalShortcut, true); | ||
| 303 | document.removeEventListener('keyup', blockGlobalShortcut, true); | 398 | document.removeEventListener('keyup', blockGlobalShortcut, true); | ||
| 304 | document.removeEventListener('keypress', blockGlobalShortcut, true); | 399 | document.removeEventListener('keypress', blockGlobalShortcut, true); | ||
| 305 | - | delete window.__dumber_global_shortcuts_protection; | 400 | + | delete window.__dumber_shortcuts_protection_version; |
| 306 | delete window.__dumber_cleanup_global_shortcuts; | 401 | delete window.__dumber_cleanup_global_shortcuts; | ||
| 307 | console.log('[dumber] Global shortcuts protection disabled'); | 402 | console.log('[dumber] Global shortcuts protection disabled'); | ||
| 308 | }; | 403 | }; | ||
| 309 | 404 | ||||
| 310 | - | console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts'); | 405 | + | console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts (v' + newVersion + ')'); |
| 406 | + | } else { | |||
| 407 | + | console.log('[dumber] Global shortcuts protection already up to date (v' + currentVersion + ')'); | |||
| 311 | } | 408 | } | ||
| 312 | })(); | 409 | })(); | ||
| 313 | ` | 410 | ` | ||
M pkg/webkit/webview_cgo.go +7 -0
| @@ -1614,6 +1614,10 @@ func (w *WebView) Destroy() error { | |||
| 1614 | 1614 | ||
| 1615 | 1615 | w.releaseNativeWidgets() | |
| 1616 | 1616 | w.destroyed = true | |
| 1617 | + | ||
| 1618 | + | // Unregister from dynamic protection updates | |
| 1619 | + | UnregisterActiveWebView(w) | |
| 1620 | + | ||
| 1617 | 1621 | unregisterView(w.id) | |
| 1618 | 1622 | return nil | |
| 1619 | 1623 | } | |
| @@ -2028,6 +2032,9 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||
| 2028 | 2032 | log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err) | |
| 2029 | 2033 | } | |
| 2030 | 2034 | ||
| 2035 | + | // Register this WebView for dynamic protection updates | |
| 2036 | + | RegisterActiveWebView(w) | |
| 2037 | + | ||
| 2031 | 2038 | // No JS fallback bridge — native UCM is active | |
| 2032 | 2039 | } | |
| 2033 | 2040 | ||
| @@ -1614,6 +1614,10 @@ func (w *WebView) Destroy() error { | |||||
| 1614 | 1614 | ||||
| 1615 | w.releaseNativeWidgets() | 1615 | w.releaseNativeWidgets() | ||
| 1616 | w.destroyed = true | 1616 | w.destroyed = true | ||
| 1617 | + | ||||
| 1618 | + | // Unregister from dynamic protection updates | |||
| 1619 | + | UnregisterActiveWebView(w) | |||
| 1620 | + | ||||
| 1617 | unregisterView(w.id) | 1621 | unregisterView(w.id) | ||
| 1618 | return nil | 1622 | return nil | ||
| 1619 | } | 1623 | } | ||
| @@ -2028,6 +2032,9 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||||
| 2028 | log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err) | 2032 | log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err) | ||
| 2029 | } | 2033 | } | ||
| 2030 | 2034 | ||||
| 2035 | + | // Register this WebView for dynamic protection updates | |||
| 2036 | + | RegisterActiveWebView(w) | |||
| 2037 | + | ||||
| 2031 | // No JS fallback bridge — native UCM is active | 2038 | // No JS fallback bridge — native UCM is active | ||
| 2032 | } | 2039 | } | ||
| 2033 | 2040 | ||||
M pkg/webkit/window_shortcuts.go +7 -2
| @@ -71,8 +71,13 @@ func (ws *WindowShortcuts) RegisterGlobalShortcut(key string, callback func()) e | |||
| 71 | 71 | return err | |
| 72 | 72 | } | |
| 73 | 73 | ||
| 74 | - | // Window shortcuts should NOT be added to global blocking registry | |
| 75 | - | // They need to bubble up to GTK for proper handling | |
| 74 | + | // Add Alt+Arrow shortcuts to global blocking registry with callback handle for C-level triggering | |
| 75 | + | if strings.HasPrefix(key, "alt+Arrow") { | |
| 76 | + | RegisterGlobalShortcutWithHandle(key, handle) | |
| 77 | + | } else { | |
| 78 | + | // For non-Alt+Arrow shortcuts, just register for blocking without callback handle | |
| 79 | + | RegisterGlobalShortcut(key) | |
| 80 | + | } | |
| 76 | 81 | ||
| 77 | 82 | log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey) | |
| 78 | 83 | return nil | |
| @@ -71,8 +71,13 @@ func (ws *WindowShortcuts) RegisterGlobalShortcut(key string, callback func()) e | |||||
| 71 | return err | 71 | return err | ||
| 72 | } | 72 | } | ||
| 73 | 73 | ||||
| 74 | - | // Window shortcuts should NOT be added to global blocking registry | 74 | + | // Add Alt+Arrow shortcuts to global blocking registry with callback handle for C-level triggering |
| 75 | - | // They need to bubble up to GTK for proper handling | 75 | + | if strings.HasPrefix(key, "alt+Arrow") { |
| 76 | + | RegisterGlobalShortcutWithHandle(key, handle) | |||
| 77 | + | } else { | |||
| 78 | + | // For non-Alt+Arrow shortcuts, just register for blocking without callback handle | |||
| 79 | + | RegisterGlobalShortcut(key) | |||
| 80 | + | } | |||
| 76 | 81 | ||||
| 77 | log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey) | 82 | log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey) | ||
| 78 | return nil | 83 | return nil | ||