From 319c5cd5bdf1e9079c7b3546b40477544321640d Mon Sep 17 00:00:00 2001 From: Federico Jeanne Date: Wed, 22 Jul 2026 09:54:49 +0200 Subject: [PATCH] KeyBindingDispatcher: suppress duplicate command execution for same native key event (Linux/GTK) On GTK (Linux), a single physical key press can, in some situations, be delivered to KeyBindingDispatcher twice for the very same native event: once synchronously via an SWT.Traverse event dispatched from within the native gtk3_key_press_event handling, and once more later via the async message queue. Both deliveries resolve to and execute the exact same bound command, which for page-traversal shortcuts like Ctrl+PageUp / Ctrl+PageDown (Next/Previous Editor) causes an extra tab to be skipped, since the CTabFolder selection is advanced twice per key press. This does not reproduce on Windows or macOS, only on GTK. Fix: track the last executed command together with the native timestamp (Event#time) of its triggering event in executeCommand(...), and skip (without re-executing the handler) a call that matches both the same command and the same event timestamp, since that combination can only happen when the very same native key event is redelivered. Genuinely distinct key presses - including fast auto-repeat - always carry different timestamps, so normal navigation and repeated shortcuts are unaffected. This is reported as the 'bonus' issue in eclipse-platform/eclipse.platform.ui#4135 (Javadoc View tab cycling two tabs per Ctrl+PageUp/PageDown on Linux). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../bindings/keys/KeyBindingDispatcher.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/bundles/org.eclipse.e4.ui.bindings/src/org/eclipse/e4/ui/bindings/keys/KeyBindingDispatcher.java b/bundles/org.eclipse.e4.ui.bindings/src/org/eclipse/e4/ui/bindings/keys/KeyBindingDispatcher.java index 36f1905234c..47260981b90 100644 --- a/bundles/org.eclipse.e4.ui.bindings/src/org/eclipse/e4/ui/bindings/keys/KeyBindingDispatcher.java +++ b/bundles/org.eclipse.e4.ui.bindings/src/org/eclipse/e4/ui/bindings/keys/KeyBindingDispatcher.java @@ -267,6 +267,21 @@ private static boolean isOutOfOrderKey(List keyStrokes) { @Optional private Logger logger; + /** + * The command executed the last time {@link #executeCommand(ParameterizedCommand, Event)} was + * called, together with the native timestamp of the triggering event. On some platforms (e.g. + * GTK on Linux) a single physical key press can be delivered to the dispatcher more than once + * for the same native event (for example, once as an SWT.Traverse event and once + * as a redundant SWT.KeyDown event), which would otherwise execute the resulting + * command twice, e.g. skipping over an extra tab when navigating with Ctrl+PageUp/PageDown. + * These fields are used to detect and suppress such a duplicate re-delivery of the very same + * native key event, identified by its (platform-provided) timestamp. + * + * @see #isDuplicateKeyEvent(ParameterizedCommand, Event) + */ + private ParameterizedCommand lastExecutedCommand; + private int lastExecutedEventTime; + /** * Performs the actual execution of the command by looking up the current handler from the * command manager. If there is a handler and it is enabled, then it tries the actual execution. @@ -292,6 +307,14 @@ public final boolean executeCommand(final ParameterizedCommand parameterizedComm throw new NotEnabledException("Command should have been disabled via activity: " + parameterizedCommand); //$NON-NLS-1$ } + if (isDuplicateKeyEvent(parameterizedCommand, trigger)) { + if (isTracingEnabled()) { + logger.trace("Suppressed duplicate execution of " + parameterizedCommand + " for the same native event " //$NON-NLS-1$ //$NON-NLS-2$ + + "(time=" + trigger.time + ") in " + describe(context)); //$NON-NLS-1$ //$NON-NLS-2$ + } + return true; + } + final EHandlerService handlerService = getHandlerService(); final Command command = parameterizedCommand.getCommand(); @@ -324,6 +347,37 @@ public final boolean executeCommand(final ParameterizedCommand parameterizedComm return (commandDefined && commandHandled); } + /** + * Detects whether the given command execution is a duplicate re-delivery of the same native + * key event that was already used to execute this very command. On some platforms (currently + * known to affect GTK on Linux) a single physical key press can reach the dispatcher twice, + * e.g. once via an SWT.Traverse event and once more via a subsequently generated + * SWT.KeyDown event, both carrying the native event's original timestamp. Without + * this guard, such duplicate delivery would execute the resulting command (e.g. "Next Editor" + * bound to Ctrl+PageDown) twice for a single key press, which is, for example, visible as + * navigation skipping over an extra tab. + *

+ * As a side effect, this method records the given command/trigger pair as the last executed + * one, so that a subsequent call can be compared against it. + * + * @param parameterizedCommand + * the command about to be executed; must not be null. + * @param trigger + * the triggering event; may be null, in which case no event was + * involved and de-duplication does not apply. + * @return true if this looks like a duplicate delivery of the same native event + * that already triggered the same command; false otherwise. + */ + private boolean isDuplicateKeyEvent(final ParameterizedCommand parameterizedCommand, final Event trigger) { + if (trigger == null) { + return false; + } + boolean duplicate = parameterizedCommand == lastExecutedCommand && trigger.time == lastExecutedEventTime; + lastExecutedCommand = parameterizedCommand; + lastExecutedEventTime = trigger.time; + return duplicate; + } + private boolean handleCommandExecution(final ParameterizedCommand parameterizedCommand, final EHandlerService handlerService, final Event trigger, Object handler) { final IEclipseContext staticContext = createContext(trigger);