fix(input): hold global shortcuts until release
- commit
- baf031ae1e4d36dcc608de4b2324cb5719bbe61e
- author
- brice · · 2026-05-24 13:29 UTC
- parent
- e5061b6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +117 -15
M internal/ui/input/global_shortcuts.go +83 -15
| @@ -19,13 +19,16 @@ import ( | |||
| 19 | 19 | // even when WebView has focus. It uses GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL | |
| 20 | 20 | // to intercept shortcuts before they reach the WebView. | |
| 21 | 21 | type GlobalShortcutHandler struct { | |
| 22 | - | controller *gtk.ShortcutController | |
| 23 | - | window *gtk.ApplicationWindow | |
| 24 | - | kbHandler *KeyboardHandler | |
| 25 | - | onAction ActionHandler | |
| 26 | - | ctx context.Context | |
| 27 | - | registered map[KeyBinding]Action | |
| 28 | - | lastDispatchAt map[Action]time.Time | |
| 22 | + | controller *gtk.ShortcutController | |
| 23 | + | releaseController *gtk.EventControllerKey | |
| 24 | + | window *gtk.ApplicationWindow | |
| 25 | + | kbHandler *KeyboardHandler | |
| 26 | + | onAction ActionHandler | |
| 27 | + | ctx context.Context | |
| 28 | + | registered map[KeyBinding]Action | |
| 29 | + | lastDispatchAt map[Action]time.Time | |
| 30 | + | heldShortcuts map[globalShortcutHoldKey]time.Time | |
| 31 | + | keyReleasedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) | |
| 29 | 32 | // generation is mutated only from the GTK main thread alongside controller replacement. | |
| 30 | 33 | generation uint64 | |
| 31 | 34 | ||
| @@ -47,24 +50,35 @@ func NewGlobalShortcutHandler( | |||
| 47 | 50 | log.Debug().Msg("creating global shortcut handler") | |
| 48 | 51 | ||
| 49 | 52 | h := &GlobalShortcutHandler{ | |
| 50 | - | controller: gtk.NewShortcutController(), | |
| 51 | - | window: window, | |
| 52 | - | kbHandler: kbHandler, | |
| 53 | - | onAction: onAction, | |
| 54 | - | ctx: ctx, | |
| 55 | - | callbacks: make([]gtk.ShortcutFunc, 0), | |
| 56 | - | registered: make(map[KeyBinding]Action), | |
| 57 | - | lastDispatchAt: make(map[Action]time.Time), | |
| 53 | + | controller: gtk.NewShortcutController(), | |
| 54 | + | releaseController: gtk.NewEventControllerKey(), | |
| 55 | + | window: window, | |
| 56 | + | kbHandler: kbHandler, | |
| 57 | + | onAction: onAction, | |
| 58 | + | ctx: ctx, | |
| 59 | + | callbacks: make([]gtk.ShortcutFunc, 0), | |
| 60 | + | registered: make(map[KeyBinding]Action), | |
| 61 | + | lastDispatchAt: make(map[Action]time.Time), | |
| 62 | + | heldShortcuts: make(map[globalShortcutHoldKey]time.Time), | |
| 58 | 63 | } | |
| 59 | 64 | ||
| 60 | 65 | if h.controller == nil { | |
| 61 | 66 | log.Error().Msg("failed to create shortcut controller") | |
| 62 | 67 | return nil | |
| 63 | 68 | } | |
| 69 | + | if h.releaseController == nil { | |
| 70 | + | log.Error().Msg("failed to create global shortcut release controller") | |
| 71 | + | return nil | |
| 72 | + | } | |
| 64 | 73 | ||
| 65 | 74 | // Set global scope - this is the key to making shortcuts work | |
| 66 | 75 | // even when WebView has focus | |
| 67 | 76 | h.controller.SetScope(gtk.ShortcutScopeGlobalValue) | |
| 77 | + | h.releaseController.SetPropagationPhase(gtk.PhaseCaptureValue) | |
| 78 | + | h.keyReleasedCb = func(_ gtk.EventControllerKey, keyval uint, keycode uint, _ gdk.ModifierType) { | |
| 79 | + | h.releaseHeldGlobalShortcuts(keyval, keycode) | |
| 80 | + | } | |
| 81 | + | h.releaseController.ConnectKeyReleased(&h.keyReleasedCb) | |
| 68 | 82 | ||
| 69 | 83 | // Register Alt+1 through Alt+9 for tab switching | |
| 70 | 84 | tabActions := []Action{ | |
| @@ -157,6 +171,7 @@ func NewGlobalShortcutHandler( | |||
| 157 | 171 | } | |
| 158 | 172 | ||
| 159 | 173 | // Attach to window | |
| 174 | + | window.AddController(&h.releaseController.EventController) | |
| 160 | 175 | window.AddController(&h.controller.EventController) | |
| 161 | 176 | ||
| 162 | 177 | log.Debug(). | |
| @@ -217,6 +232,13 @@ func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.Modi | |||
| 217 | 232 | Msg("global shortcut ignored without current key event") | |
| 218 | 233 | return true | |
| 219 | 234 | } | |
| 235 | + | if h.suppressHeldShortcut(actionToDispatch, eventInfo, time.Now()) { | |
| 236 | + | log.Trace(). | |
| 237 | + | Str("action", string(actionToDispatch)). | |
| 238 | + | Str("shortcut", formatBinding(bindingForLog)). | |
| 239 | + | Msg("held global shortcut repeat suppressed") | |
| 240 | + | return true | |
| 241 | + | } | |
| 220 | 242 | if h.suppressRepeatedShortcut(actionToDispatch, time.Now()) { | |
| 221 | 243 | log.Trace(). | |
| 222 | 244 | Str("action", string(actionToDispatch)). | |
| @@ -277,6 +299,11 @@ func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.Modi | |||
| 277 | 299 | } | |
| 278 | 300 | ||
| 279 | 301 | // globalShortcutActionMap returns a fresh map of workspace action names to global shortcut actions. | |
| 302 | + | type globalShortcutHoldKey struct { | |
| 303 | + | keyval uint | |
| 304 | + | keycode uint | |
| 305 | + | } | |
| 306 | + | ||
| 280 | 307 | type globalShortcutEventInfo struct { | |
| 281 | 308 | hasCurrentEvent bool | |
| 282 | 309 | eventType gdk.EventType | |
| @@ -432,6 +459,7 @@ func (h *GlobalShortcutHandler) ReloadShortcuts(ctx context.Context, workspace * | |||
| 432 | 459 | h.callbacks = make([]gtk.ShortcutFunc, 0) | |
| 433 | 460 | h.registered = make(map[KeyBinding]Action) | |
| 434 | 461 | h.lastDispatchAt = make(map[Action]time.Time) | |
| 462 | + | h.heldShortcuts = make(map[globalShortcutHoldKey]time.Time) | |
| 435 | 463 | ||
| 436 | 464 | // Re-register hardcoded shortcuts (Alt+1-9, Alt+0, Alt+Tab, Ctrl+Shift+S) | |
| 437 | 465 | tabActions := []Action{ | |
| @@ -501,10 +529,16 @@ func (h *GlobalShortcutHandler) Detach() { | |||
| 501 | 529 | if h.window != nil && h.controller != nil { | |
| 502 | 530 | h.window.RemoveController(&h.controller.EventController) | |
| 503 | 531 | } | |
| 532 | + | if h.window != nil && h.releaseController != nil { | |
| 533 | + | h.window.RemoveController(&h.releaseController.EventController) | |
| 534 | + | } | |
| 504 | 535 | h.controller = nil | |
| 536 | + | h.releaseController = nil | |
| 537 | + | h.keyReleasedCb = nil | |
| 505 | 538 | h.callbacks = nil | |
| 506 | 539 | h.registered = nil | |
| 507 | 540 | h.lastDispatchAt = nil | |
| 541 | + | h.heldShortcuts = nil | |
| 508 | 542 | } | |
| 509 | 543 | ||
| 510 | 544 | const globalShortcutRepeatSuppressWindow = 250 * time.Millisecond | |
| @@ -517,6 +551,36 @@ func (h *GlobalShortcutHandler) isActiveWindowShortcutHandler() bool { | |||
| 517 | 551 | return h != nil && h.controller != nil && h.window != nil && h.window.IsActive() | |
| 518 | 552 | } | |
| 519 | 553 | ||
| 554 | + | func (h *GlobalShortcutHandler) suppressHeldShortcut(action Action, info globalShortcutEventInfo, now time.Time) bool { | |
| 555 | + | if h == nil || !isHeldGlobalShortcutSuppressed(action) || !info.hasCurrentEvent { | |
| 556 | + | return false | |
| 557 | + | } | |
| 558 | + | if h.heldShortcuts == nil { | |
| 559 | + | return true | |
| 560 | + | } | |
| 561 | + | key := globalShortcutHoldKey{keyval: normalizeKeyval(info.eventKeyval), keycode: info.eventKeycode} | |
| 562 | + | if key.keyval == 0 && key.keycode == 0 { | |
| 563 | + | return false | |
| 564 | + | } | |
| 565 | + | if _, ok := h.heldShortcuts[key]; ok { | |
| 566 | + | return true | |
| 567 | + | } | |
| 568 | + | h.heldShortcuts[key] = now | |
| 569 | + | return false | |
| 570 | + | } | |
| 571 | + | ||
| 572 | + | func (h *GlobalShortcutHandler) releaseHeldGlobalShortcuts(keyval, keycode uint) { | |
| 573 | + | if h == nil || len(h.heldShortcuts) == 0 { | |
| 574 | + | return | |
| 575 | + | } | |
| 576 | + | keyval = normalizeKeyval(keyval) | |
| 577 | + | for key := range h.heldShortcuts { | |
| 578 | + | if (keyval != 0 && key.keyval == keyval) || (keycode != 0 && key.keycode == keycode) { | |
| 579 | + | delete(h.heldShortcuts, key) | |
| 580 | + | } | |
| 581 | + | } | |
| 582 | + | } | |
| 583 | + | ||
| 520 | 584 | func (h *GlobalShortcutHandler) suppressRepeatedShortcut(action Action, now time.Time) bool { | |
| 521 | 585 | if h == nil { | |
| 522 | 586 | return false | |
| @@ -538,6 +602,10 @@ func (h *GlobalShortcutHandler) suppressRepeatedShortcut(action Action, now time | |||
| 538 | 602 | return false | |
| 539 | 603 | } | |
| 540 | 604 | ||
| 605 | + | func isHeldGlobalShortcutSuppressed(action Action) bool { | |
| 606 | + | return isRepeatedGlobalShortcutSuppressed(action) || isModeAction(action) | |
| 607 | + | } | |
| 608 | + | ||
| 541 | 609 | func isRepeatedGlobalShortcutSuppressed(action Action) bool { | |
| 542 | 610 | switch action { | |
| 543 | 611 | case ActionGoBack, | |
| @@ -19,13 +19,16 @@ import ( | |||||
| 19 | // even when WebView has focus. It uses GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL | 19 | // even when WebView has focus. It uses GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL | ||
| 20 | // to intercept shortcuts before they reach the WebView. | 20 | // to intercept shortcuts before they reach the WebView. | ||
| 21 | type GlobalShortcutHandler struct { | 21 | type GlobalShortcutHandler struct { | ||
| 22 | - | controller *gtk.ShortcutController | 22 | + | controller *gtk.ShortcutController |
| 23 | - | window *gtk.ApplicationWindow | 23 | + | releaseController *gtk.EventControllerKey |
| 24 | - | kbHandler *KeyboardHandler | 24 | + | window *gtk.ApplicationWindow |
| 25 | - | onAction ActionHandler | 25 | + | kbHandler *KeyboardHandler |
| 26 | - | ctx context.Context | 26 | + | onAction ActionHandler |
| 27 | - | registered map[KeyBinding]Action | 27 | + | ctx context.Context |
| 28 | - | lastDispatchAt map[Action]time.Time | 28 | + | registered map[KeyBinding]Action |
| 29 | + | lastDispatchAt map[Action]time.Time | |||
| 30 | + | heldShortcuts map[globalShortcutHoldKey]time.Time | |||
| 31 | + | keyReleasedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) | |||
| 29 | // generation is mutated only from the GTK main thread alongside controller replacement. | 32 | // generation is mutated only from the GTK main thread alongside controller replacement. | ||
| 30 | generation uint64 | 33 | generation uint64 | ||
| 31 | 34 | ||||
| @@ -47,24 +50,35 @@ func NewGlobalShortcutHandler( | |||||
| 47 | log.Debug().Msg("creating global shortcut handler") | 50 | log.Debug().Msg("creating global shortcut handler") | ||
| 48 | 51 | ||||
| 49 | h := &GlobalShortcutHandler{ | 52 | h := &GlobalShortcutHandler{ | ||
| 50 | - | controller: gtk.NewShortcutController(), | 53 | + | controller: gtk.NewShortcutController(), |
| 51 | - | window: window, | 54 | + | releaseController: gtk.NewEventControllerKey(), |
| 52 | - | kbHandler: kbHandler, | 55 | + | window: window, |
| 53 | - | onAction: onAction, | 56 | + | kbHandler: kbHandler, |
| 54 | - | ctx: ctx, | 57 | + | onAction: onAction, |
| 55 | - | callbacks: make([]gtk.ShortcutFunc, 0), | 58 | + | ctx: ctx, |
| 56 | - | registered: make(map[KeyBinding]Action), | 59 | + | callbacks: make([]gtk.ShortcutFunc, 0), |
| 57 | - | lastDispatchAt: make(map[Action]time.Time), | 60 | + | registered: make(map[KeyBinding]Action), |
| 61 | + | lastDispatchAt: make(map[Action]time.Time), | |||
| 62 | + | heldShortcuts: make(map[globalShortcutHoldKey]time.Time), | |||
| 58 | } | 63 | } | ||
| 59 | 64 | ||||
| 60 | if h.controller == nil { | 65 | if h.controller == nil { | ||
| 61 | log.Error().Msg("failed to create shortcut controller") | 66 | log.Error().Msg("failed to create shortcut controller") | ||
| 62 | return nil | 67 | return nil | ||
| 63 | } | 68 | } | ||
| 69 | + | if h.releaseController == nil { | |||
| 70 | + | log.Error().Msg("failed to create global shortcut release controller") | |||
| 71 | + | return nil | |||
| 72 | + | } | |||
| 64 | 73 | ||||
| 65 | // Set global scope - this is the key to making shortcuts work | 74 | // Set global scope - this is the key to making shortcuts work | ||
| 66 | // even when WebView has focus | 75 | // even when WebView has focus | ||
| 67 | h.controller.SetScope(gtk.ShortcutScopeGlobalValue) | 76 | h.controller.SetScope(gtk.ShortcutScopeGlobalValue) | ||
| 77 | + | h.releaseController.SetPropagationPhase(gtk.PhaseCaptureValue) | |||
| 78 | + | h.keyReleasedCb = func(_ gtk.EventControllerKey, keyval uint, keycode uint, _ gdk.ModifierType) { | |||
| 79 | + | h.releaseHeldGlobalShortcuts(keyval, keycode) | |||
| 80 | + | } | |||
| 81 | + | h.releaseController.ConnectKeyReleased(&h.keyReleasedCb) | |||
| 68 | 82 | ||||
| 69 | // Register Alt+1 through Alt+9 for tab switching | 83 | // Register Alt+1 through Alt+9 for tab switching | ||
| 70 | tabActions := []Action{ | 84 | tabActions := []Action{ | ||
| @@ -157,6 +171,7 @@ func NewGlobalShortcutHandler( | |||||
| 157 | } | 171 | } | ||
| 158 | 172 | ||||
| 159 | // Attach to window | 173 | // Attach to window | ||
| 174 | + | window.AddController(&h.releaseController.EventController) | |||
| 160 | window.AddController(&h.controller.EventController) | 175 | window.AddController(&h.controller.EventController) | ||
| 161 | 176 | ||||
| 162 | log.Debug(). | 177 | log.Debug(). | ||
| @@ -217,6 +232,13 @@ func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.Modi | |||||
| 217 | Msg("global shortcut ignored without current key event") | 232 | Msg("global shortcut ignored without current key event") | ||
| 218 | return true | 233 | return true | ||
| 219 | } | 234 | } | ||
| 235 | + | if h.suppressHeldShortcut(actionToDispatch, eventInfo, time.Now()) { | |||
| 236 | + | log.Trace(). | |||
| 237 | + | Str("action", string(actionToDispatch)). | |||
| 238 | + | Str("shortcut", formatBinding(bindingForLog)). | |||
| 239 | + | Msg("held global shortcut repeat suppressed") | |||
| 240 | + | return true | |||
| 241 | + | } | |||
| 220 | if h.suppressRepeatedShortcut(actionToDispatch, time.Now()) { | 242 | if h.suppressRepeatedShortcut(actionToDispatch, time.Now()) { | ||
| 221 | log.Trace(). | 243 | log.Trace(). | ||
| 222 | Str("action", string(actionToDispatch)). | 244 | Str("action", string(actionToDispatch)). | ||
| @@ -277,6 +299,11 @@ func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.Modi | |||||
| 277 | } | 299 | } | ||
| 278 | 300 | ||||
| 279 | // globalShortcutActionMap returns a fresh map of workspace action names to global shortcut actions. | 301 | // globalShortcutActionMap returns a fresh map of workspace action names to global shortcut actions. | ||
| 302 | + | type globalShortcutHoldKey struct { | |||
| 303 | + | keyval uint | |||
| 304 | + | keycode uint | |||
| 305 | + | } | |||
| 306 | + | ||||
| 280 | type globalShortcutEventInfo struct { | 307 | type globalShortcutEventInfo struct { | ||
| 281 | hasCurrentEvent bool | 308 | hasCurrentEvent bool | ||
| 282 | eventType gdk.EventType | 309 | eventType gdk.EventType | ||
| @@ -432,6 +459,7 @@ func (h *GlobalShortcutHandler) ReloadShortcuts(ctx context.Context, workspace * | |||||
| 432 | h.callbacks = make([]gtk.ShortcutFunc, 0) | 459 | h.callbacks = make([]gtk.ShortcutFunc, 0) | ||
| 433 | h.registered = make(map[KeyBinding]Action) | 460 | h.registered = make(map[KeyBinding]Action) | ||
| 434 | h.lastDispatchAt = make(map[Action]time.Time) | 461 | h.lastDispatchAt = make(map[Action]time.Time) | ||
| 462 | + | h.heldShortcuts = make(map[globalShortcutHoldKey]time.Time) | |||
| 435 | 463 | ||||
| 436 | // Re-register hardcoded shortcuts (Alt+1-9, Alt+0, Alt+Tab, Ctrl+Shift+S) | 464 | // Re-register hardcoded shortcuts (Alt+1-9, Alt+0, Alt+Tab, Ctrl+Shift+S) | ||
| 437 | tabActions := []Action{ | 465 | tabActions := []Action{ | ||
| @@ -501,10 +529,16 @@ func (h *GlobalShortcutHandler) Detach() { | |||||
| 501 | if h.window != nil && h.controller != nil { | 529 | if h.window != nil && h.controller != nil { | ||
| 502 | h.window.RemoveController(&h.controller.EventController) | 530 | h.window.RemoveController(&h.controller.EventController) | ||
| 503 | } | 531 | } | ||
| 532 | + | if h.window != nil && h.releaseController != nil { | |||
| 533 | + | h.window.RemoveController(&h.releaseController.EventController) | |||
| 534 | + | } | |||
| 504 | h.controller = nil | 535 | h.controller = nil | ||
| 536 | + | h.releaseController = nil | |||
| 537 | + | h.keyReleasedCb = nil | |||
| 505 | h.callbacks = nil | 538 | h.callbacks = nil | ||
| 506 | h.registered = nil | 539 | h.registered = nil | ||
| 507 | h.lastDispatchAt = nil | 540 | h.lastDispatchAt = nil | ||
| 541 | + | h.heldShortcuts = nil | |||
| 508 | } | 542 | } | ||
| 509 | 543 | ||||
| 510 | const globalShortcutRepeatSuppressWindow = 250 * time.Millisecond | 544 | const globalShortcutRepeatSuppressWindow = 250 * time.Millisecond | ||
| @@ -517,6 +551,36 @@ func (h *GlobalShortcutHandler) isActiveWindowShortcutHandler() bool { | |||||
| 517 | return h != nil && h.controller != nil && h.window != nil && h.window.IsActive() | 551 | return h != nil && h.controller != nil && h.window != nil && h.window.IsActive() | ||
| 518 | } | 552 | } | ||
| 519 | 553 | ||||
| 554 | + | func (h *GlobalShortcutHandler) suppressHeldShortcut(action Action, info globalShortcutEventInfo, now time.Time) bool { | |||
| 555 | + | if h == nil || !isHeldGlobalShortcutSuppressed(action) || !info.hasCurrentEvent { | |||
| 556 | + | return false | |||
| 557 | + | } | |||
| 558 | + | if h.heldShortcuts == nil { | |||
| 559 | + | return true | |||
| 560 | + | } | |||
| 561 | + | key := globalShortcutHoldKey{keyval: normalizeKeyval(info.eventKeyval), keycode: info.eventKeycode} | |||
| 562 | + | if key.keyval == 0 && key.keycode == 0 { | |||
| 563 | + | return false | |||
| 564 | + | } | |||
| 565 | + | if _, ok := h.heldShortcuts[key]; ok { | |||
| 566 | + | return true | |||
| 567 | + | } | |||
| 568 | + | h.heldShortcuts[key] = now | |||
| 569 | + | return false | |||
| 570 | + | } | |||
| 571 | + | ||||
| 572 | + | func (h *GlobalShortcutHandler) releaseHeldGlobalShortcuts(keyval, keycode uint) { | |||
| 573 | + | if h == nil || len(h.heldShortcuts) == 0 { | |||
| 574 | + | return | |||
| 575 | + | } | |||
| 576 | + | keyval = normalizeKeyval(keyval) | |||
| 577 | + | for key := range h.heldShortcuts { | |||
| 578 | + | if (keyval != 0 && key.keyval == keyval) || (keycode != 0 && key.keycode == keycode) { | |||
| 579 | + | delete(h.heldShortcuts, key) | |||
| 580 | + | } | |||
| 581 | + | } | |||
| 582 | + | } | |||
| 583 | + | ||||
| 520 | func (h *GlobalShortcutHandler) suppressRepeatedShortcut(action Action, now time.Time) bool { | 584 | func (h *GlobalShortcutHandler) suppressRepeatedShortcut(action Action, now time.Time) bool { | ||
| 521 | if h == nil { | 585 | if h == nil { | ||
| 522 | return false | 586 | return false | ||
| @@ -538,6 +602,10 @@ func (h *GlobalShortcutHandler) suppressRepeatedShortcut(action Action, now time | |||||
| 538 | return false | 602 | return false | ||
| 539 | } | 603 | } | ||
| 540 | 604 | ||||
| 605 | + | func isHeldGlobalShortcutSuppressed(action Action) bool { | |||
| 606 | + | return isRepeatedGlobalShortcutSuppressed(action) || isModeAction(action) | |||
| 607 | + | } | |||
| 608 | + | ||||
| 541 | func isRepeatedGlobalShortcutSuppressed(action Action) bool { | 609 | func isRepeatedGlobalShortcutSuppressed(action Action) bool { | ||
| 542 | switch action { | 610 | switch action { | ||
| 543 | case ActionGoBack, | 611 | case ActionGoBack, | ||
M internal/ui/input/global_shortcuts_test.go +34 -0
| @@ -188,6 +188,40 @@ func TestGlobalShortcutHandlerSuppressesActionSwitchLastTab(t *testing.T) { | |||
| 188 | 188 | } | |
| 189 | 189 | } | |
| 190 | 190 | ||
| 191 | + | func TestGlobalShortcutHandlerSuppressesHeldGlobalShortcutUntilRelease(t *testing.T) { | |
| 192 | + | h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]time.Time)} | |
| 193 | + | info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('f'), eventKeycode: 41} | |
| 194 | + | ||
| 195 | + | if h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(100, 0)) { | |
| 196 | + | t.Fatal("first held global shortcut dispatch was suppressed") | |
| 197 | + | } | |
| 198 | + | if !h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(101, 0)) { | |
| 199 | + | t.Fatal("held global shortcut repeat after cooldown was not suppressed") | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | h.releaseHeldGlobalShortcuts(uint('x'), 0) | |
| 203 | + | if !h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(102, 0)) { | |
| 204 | + | t.Fatal("unrelated key release re-armed held global shortcut") | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | h.releaseHeldGlobalShortcuts(uint('f'), 0) | |
| 208 | + | if h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(103, 0)) { | |
| 209 | + | t.Fatal("matching key release did not re-arm held global shortcut") | |
| 210 | + | } | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | func TestGlobalShortcutHandlerSuppressesHeldModeAction(t *testing.T) { | |
| 214 | + | h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]time.Time)} | |
| 215 | + | info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('p'), eventKeycode: 33} | |
| 216 | + | ||
| 217 | + | if h.suppressHeldShortcut(ActionEnterPaneMode, info, time.Unix(100, 0)) { | |
| 218 | + | t.Fatal("first mode shortcut dispatch was suppressed") | |
| 219 | + | } | |
| 220 | + | if !h.suppressHeldShortcut(ActionEnterPaneMode, info, time.Unix(101, 0)) { | |
| 221 | + | t.Fatal("held mode shortcut repeat was not suppressed") | |
| 222 | + | } | |
| 223 | + | } | |
| 224 | + | ||
| 191 | 225 | func TestShouldDispatchGlobalShortcutEventRequiresCurrentKeyEvent(t *testing.T) { | |
| 192 | 226 | if shouldDispatchGlobalShortcutEvent(globalShortcutEventInfo{}) { | |
| 193 | 227 | t.Fatal("global shortcut without current event should not dispatch") | |
| @@ -188,6 +188,40 @@ func TestGlobalShortcutHandlerSuppressesActionSwitchLastTab(t *testing.T) { | |||||
| 188 | } | 188 | } | ||
| 189 | } | 189 | } | ||
| 190 | 190 | ||||
| 191 | + | func TestGlobalShortcutHandlerSuppressesHeldGlobalShortcutUntilRelease(t *testing.T) { | |||
| 192 | + | h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]time.Time)} | |||
| 193 | + | info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('f'), eventKeycode: 41} | |||
| 194 | + | ||||
| 195 | + | if h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(100, 0)) { | |||
| 196 | + | t.Fatal("first held global shortcut dispatch was suppressed") | |||
| 197 | + | } | |||
| 198 | + | if !h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(101, 0)) { | |||
| 199 | + | t.Fatal("held global shortcut repeat after cooldown was not suppressed") | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | h.releaseHeldGlobalShortcuts(uint('x'), 0) | |||
| 203 | + | if !h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(102, 0)) { | |||
| 204 | + | t.Fatal("unrelated key release re-armed held global shortcut") | |||
| 205 | + | } | |||
| 206 | + | ||||
| 207 | + | h.releaseHeldGlobalShortcuts(uint('f'), 0) | |||
| 208 | + | if h.suppressHeldShortcut(ActionToggleFloatingPane, info, time.Unix(103, 0)) { | |||
| 209 | + | t.Fatal("matching key release did not re-arm held global shortcut") | |||
| 210 | + | } | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | func TestGlobalShortcutHandlerSuppressesHeldModeAction(t *testing.T) { | |||
| 214 | + | h := &GlobalShortcutHandler{heldShortcuts: make(map[globalShortcutHoldKey]time.Time)} | |||
| 215 | + | info := globalShortcutEventInfo{hasCurrentEvent: true, eventType: gdk.KeyPressValue, eventKeyval: uint('p'), eventKeycode: 33} | |||
| 216 | + | ||||
| 217 | + | if h.suppressHeldShortcut(ActionEnterPaneMode, info, time.Unix(100, 0)) { | |||
| 218 | + | t.Fatal("first mode shortcut dispatch was suppressed") | |||
| 219 | + | } | |||
| 220 | + | if !h.suppressHeldShortcut(ActionEnterPaneMode, info, time.Unix(101, 0)) { | |||
| 221 | + | t.Fatal("held mode shortcut repeat was not suppressed") | |||
| 222 | + | } | |||
| 223 | + | } | |||
| 224 | + | ||||
| 191 | func TestShouldDispatchGlobalShortcutEventRequiresCurrentKeyEvent(t *testing.T) { | 225 | func TestShouldDispatchGlobalShortcutEventRequiresCurrentKeyEvent(t *testing.T) { | ||
| 192 | if shouldDispatchGlobalShortcutEvent(globalShortcutEventInfo{}) { | 226 | if shouldDispatchGlobalShortcutEvent(globalShortcutEventInfo{}) { | ||
| 193 | t.Fatal("global shortcut without current event should not dispatch") | 227 | t.Fatal("global shortcut without current event should not dispatch") | ||