fix(focus): deduplicate GTK focus events with timestamp tracking
WebKitGTK nested widgets fire multiple focus enter/leave events in the same millisecond, causing duplicate logging and potential state issues. Changes: - focus_state_machine.go: Add timestamp-based deduplication in GTK controller callbacks. Skip events with same millisecond timestamp. - workspace_types.go: Add lastFocusEnterTime and lastFocusLeaveTime fields to paneNode for tracking. This prevents duplicate GTK focus events without losing real focus changes.
- commit
- d2d9ad1c612af9ccda855e620655cfa764b40684
- author
- bamen · · 2025-10-13 18:14 UTC
- committer
- bamen · · 2025-10-13 21:36 UTC
- parent
- 9a95d24
- signature
- unsigned
2 files · +17 -1
M internal/app/browser/focus_state_machine.go +15 -1
| @@ -956,14 +956,28 @@ func (fsm *FocusStateMachine) attachGTKController(node *paneNode) { | |||
| 956 | 956 | // GTK_PHASE_TARGET means we only get events for this specific widget, not bubbled events | |
| 957 | 957 | controller.SetPropagationPhase(gtk.PhaseTarget) | |
| 958 | 958 | ||
| 959 | - | // Connect focus enter/leave callbacks for this specific node | |
| 959 | + | // Connect focus enter/leave callbacks with timestamp-based deduplication | |
| 960 | 960 | controller.ConnectEnter(func() { | |
| 961 | + | // Deduplicate: WebKitGTK nested widgets can fire multiple events in same millisecond | |
| 962 | + | now := time.Now().UnixMilli() | |
| 963 | + | if node.lastFocusEnterTime == now { | |
| 964 | + | return // Duplicate event in same millisecond | |
| 965 | + | } | |
| 966 | + | node.lastFocusEnterTime = now | |
| 967 | + | ||
| 961 | 968 | log.Printf("[FSM] GTK focus enter: %p", node) | |
| 962 | 969 | // Don't automatically change focus on GTK enter - let user interactions drive this | |
| 963 | 970 | // This prevents infinite loops with our own focus changes | |
| 964 | 971 | }) | |
| 965 | 972 | ||
| 966 | 973 | controller.ConnectLeave(func() { | |
| 974 | + | // Deduplicate: WebKitGTK nested widgets can fire multiple events in same millisecond | |
| 975 | + | now := time.Now().UnixMilli() | |
| 976 | + | if node.lastFocusLeaveTime == now { | |
| 977 | + | return // Duplicate event in same millisecond | |
| 978 | + | } | |
| 979 | + | node.lastFocusLeaveTime = now | |
| 980 | + | ||
| 967 | 981 | log.Printf("[FSM] GTK focus leave: %p", node) | |
| 968 | 982 | // Similarly, don't react to GTK leave events automatically | |
| 969 | 983 | }) | |
| @@ -956,14 +956,28 @@ func (fsm *FocusStateMachine) attachGTKController(node *paneNode) { | |||||
| 956 | // GTK_PHASE_TARGET means we only get events for this specific widget, not bubbled events | 956 | // GTK_PHASE_TARGET means we only get events for this specific widget, not bubbled events | ||
| 957 | controller.SetPropagationPhase(gtk.PhaseTarget) | 957 | controller.SetPropagationPhase(gtk.PhaseTarget) | ||
| 958 | 958 | ||||
| 959 | - | // Connect focus enter/leave callbacks for this specific node | 959 | + | // Connect focus enter/leave callbacks with timestamp-based deduplication |
| 960 | controller.ConnectEnter(func() { | 960 | controller.ConnectEnter(func() { | ||
| 961 | + | // Deduplicate: WebKitGTK nested widgets can fire multiple events in same millisecond | |||
| 962 | + | now := time.Now().UnixMilli() | |||
| 963 | + | if node.lastFocusEnterTime == now { | |||
| 964 | + | return // Duplicate event in same millisecond | |||
| 965 | + | } | |||
| 966 | + | node.lastFocusEnterTime = now | |||
| 967 | + | ||||
| 961 | log.Printf("[FSM] GTK focus enter: %p", node) | 968 | log.Printf("[FSM] GTK focus enter: %p", node) | ||
| 962 | // Don't automatically change focus on GTK enter - let user interactions drive this | 969 | // Don't automatically change focus on GTK enter - let user interactions drive this | ||
| 963 | // This prevents infinite loops with our own focus changes | 970 | // This prevents infinite loops with our own focus changes | ||
| 964 | }) | 971 | }) | ||
| 965 | 972 | ||||
| 966 | controller.ConnectLeave(func() { | 973 | controller.ConnectLeave(func() { | ||
| 974 | + | // Deduplicate: WebKitGTK nested widgets can fire multiple events in same millisecond | |||
| 975 | + | now := time.Now().UnixMilli() | |||
| 976 | + | if node.lastFocusLeaveTime == now { | |||
| 977 | + | return // Duplicate event in same millisecond | |||
| 978 | + | } | |||
| 979 | + | node.lastFocusLeaveTime = now | |||
| 980 | + | ||||
| 967 | log.Printf("[FSM] GTK focus leave: %p", node) | 981 | log.Printf("[FSM] GTK focus leave: %p", node) | ||
| 968 | // Similarly, don't react to GTK leave events automatically | 982 | // Similarly, don't react to GTK leave events automatically | ||
| 969 | }) | 983 | }) | ||
M internal/app/browser/workspace_types.go +2 -0
| @@ -70,6 +70,8 @@ type paneNode struct { | |||
| 70 | 70 | pendingHoverReattach bool // Needs hover reattachment after reparent | |
| 71 | 71 | pendingFocusReattach bool // Needs focus reattachment after reparent | |
| 72 | 72 | lastHoverTime int64 // Timestamp of last hover event (for debouncing) | |
| 73 | + | lastFocusEnterTime int64 // Timestamp of last focus enter event (for deduplication) | |
| 74 | + | lastFocusLeaveTime int64 // Timestamp of last focus leave event (for deduplication) | |
| 73 | 75 | ||
| 74 | 76 | // Stacked panes support - terminal branch nodes | |
| 75 | 77 | isStacked bool // Whether this node contains stacked panes | |
| @@ -70,6 +70,8 @@ type paneNode struct { | |||||
| 70 | pendingHoverReattach bool // Needs hover reattachment after reparent | 70 | pendingHoverReattach bool // Needs hover reattachment after reparent | ||
| 71 | pendingFocusReattach bool // Needs focus reattachment after reparent | 71 | pendingFocusReattach bool // Needs focus reattachment after reparent | ||
| 72 | lastHoverTime int64 // Timestamp of last hover event (for debouncing) | 72 | lastHoverTime int64 // Timestamp of last hover event (for debouncing) | ||
| 73 | + | lastFocusEnterTime int64 // Timestamp of last focus enter event (for deduplication) | |||
| 74 | + | lastFocusLeaveTime int64 // Timestamp of last focus leave event (for deduplication) | |||
| 73 | 75 | ||||
| 74 | // Stacked panes support - terminal branch nodes | 76 | // Stacked panes support - terminal branch nodes | ||
| 75 | isStacked bool // Whether this node contains stacked panes | 77 | isStacked bool // Whether this node contains stacked panes | ||