Skip to content

Commit ec7b8b0

Browse files
HeikoKlarefedejeanne
authored andcommitted
Fix commands being executed twice due to stale isEnabled() check #4080
In commit 35f5f09, the commandHandled evaluation in KeyBindingDispatcher.executeCommand() was changed to include command.isEnabled() in addition to handler.isHandled(). This causes commands to be executed twice (e.g. Paste on a Combo widget): - command.isEnabled() reads the handler's enabled state without first calling setEnabled(evaluationContext), so it returns stale state that does not reflect the currently focused widget context. - Inside Command.executeWithChecks(), the framework calls setEnabled(event.getApplicationContext()) immediately before the same isEnabled() check, giving a fresh result. The two checks are not equivalent and can disagree for IHandler2 implementations. - When command.isEnabled() incorrectly returns false, commandHandled is false, executeCommand() returns false, and the key event is not consumed (event.doit remains true) even though the command is actually executed. The OS then processes the keystroke natively, causing a second execution of the same action. Fix 1: Revert determination of successful command handling to use only handler.isHandled(), as it was before commit 35f5f09. Fix 2: Change handleCommandExecution() to return a boolean (false if a CommandException was recorded after execution) and use that return value to update commandHandled in the synchronous non-Browser path. This restores the original behaviour where a handler that claims isHandled() but fails during execution (e.g. NotEnabledException after a fresh setEnabled() evaluation) does not eat the key event. The async Browser path is unchanged by design. Fixes #4080
1 parent 130a4ef commit ec7b8b0

2 files changed

Lines changed: 62 additions & 3 deletions

File tree

bundles/org.eclipse.e4.ui.bindings/src/org/eclipse/e4/ui/bindings/keys/KeyBindingDispatcher.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ public final boolean executeCommand(final ParameterizedCommand parameterizedComm
304304
Object obj = HandlerServiceImpl.lookUpHandler(context, command.getId());
305305
if (obj != null) {
306306
if (obj instanceof IHandler handler) {
307-
commandHandled = command.isEnabled() && handler.isHandled();
307+
commandHandled = handler.isHandled();
308308
} else {
309309
commandHandled = true;
310310
}
@@ -319,12 +319,12 @@ public final boolean executeCommand(final ParameterizedCommand parameterizedComm
319319
getDisplay()
320320
.asyncExec(() -> handleCommandExecution(parameterizedCommand, handlerService, trigger, obj));
321321
} else {
322-
handleCommandExecution(parameterizedCommand, handlerService, trigger, obj);
322+
commandHandled &= handleCommandExecution(parameterizedCommand, handlerService, trigger, obj);
323323
}
324324
return (commandDefined && commandHandled);
325325
}
326326

327-
private void handleCommandExecution(final ParameterizedCommand parameterizedCommand,
327+
private boolean handleCommandExecution(final ParameterizedCommand parameterizedCommand,
328328
final EHandlerService handlerService, final Event trigger, Object handler) {
329329
final IEclipseContext staticContext = createContext(trigger);
330330
try {
@@ -360,6 +360,7 @@ private void handleCommandExecution(final ParameterizedCommand parameterizedComm
360360
}
361361
}
362362
}
363+
return false;
363364
}
364365
} finally {
365366
staticContext.dispose();
@@ -371,6 +372,7 @@ private void handleCommandExecution(final ParameterizedCommand parameterizedComm
371372
if (keyAssistDialog != null) {
372373
keyAssistDialog.clearRememberedState();
373374
}
375+
return true;
374376
}
375377

376378
private boolean isTracingEnabled() {

tests/org.eclipse.e4.ui.bindings.tests/src/org/eclipse/e4/ui/bindings/tests/KeyDispatcherTest.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package org.eclipse.e4.ui.bindings.tests;
1717

1818
import static org.junit.jupiter.api.Assertions.*;
19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.Mockito.*;
1921
import org.junit.jupiter.api.AfterEach;
2022
import org.junit.jupiter.api.BeforeEach;
2123
import org.junit.jupiter.api.Disabled;
@@ -24,7 +26,11 @@
2426
import java.util.HashMap;
2527
import java.util.Map;
2628

29+
import org.eclipse.core.commands.AbstractHandler;
2730
import org.eclipse.core.commands.Category;
31+
import org.eclipse.core.commands.Command;
32+
import org.eclipse.core.commands.ExecutionEvent;
33+
import org.eclipse.core.commands.ExecutionException;
2834
import org.eclipse.core.commands.ParameterizedCommand;
2935
import org.eclipse.core.commands.contexts.Context;
3036
import org.eclipse.core.commands.contexts.ContextManager;
@@ -197,6 +203,57 @@ public void testExecuteOneCommand() {
197203
assertTrue(handler.q2);
198204
}
199205

206+
/**
207+
* Regression test for https://github.com/eclipse-platform/eclipse.platform.ui/issues/4080
208+
* <p>
209+
* A handler's enabled state may be stale from a previous UI-refresh cycle and return
210+
* {@code false} even though the handler is capable of handling the command and would execute
211+
* it successfully. {@link KeyBindingDispatcher#executeCommand} must return {@code true}
212+
* whenever the handler is capable of handling the command, so that the caller consumes the key
213+
* event and prevents the OS from also processing it natively — which would cause the command to
214+
* execute twice.
215+
*/
216+
@Test
217+
public void testExecuteCommandReturnsTrueWhenHandlerEnabledStateIsStale() throws Exception {
218+
// Spy on an AbstractHandler (IHandler2) whose enabled state starts stale (false) but
219+
// refreshes to true when Command.executeWithChecks() calls setEnabled(appContext).
220+
AbstractHandler staleHandler = spy(new AbstractHandler() {
221+
{
222+
setBaseEnabled(false); // stale: disabled without an execution context
223+
}
224+
225+
@Override
226+
public void setEnabled(Object evaluationContext) {
227+
setBaseEnabled(evaluationContext != null);
228+
}
229+
230+
@Override
231+
public Object execute(ExecutionEvent event) throws ExecutionException {
232+
return null;
233+
}
234+
});
235+
236+
EHandlerService hs = workbenchContext.get(EHandlerService.class);
237+
hs.activateHandler(TEST_ID1, staleHandler);
238+
239+
ECommandService cs = workbenchContext.get(ECommandService.class);
240+
ParameterizedCommand paramCmd = cs.createCommand(TEST_ID1, null);
241+
Command command = paramCmd.getCommand();
242+
command.setHandler(staleHandler);
243+
244+
assertFalse(command.isEnabled(), "Precondition: command.isEnabled() must be false (stale state)");
245+
246+
Event event = new Event();
247+
event.type = SWT.KeyDown;
248+
event.stateMask = SWT.CTRL;
249+
event.keyCode = 'A';
250+
251+
boolean result = dispatcher.executeCommand(paramCmd, event);
252+
253+
assertTrue(result, "executeCommand() must return true when the handler is capable of execution");
254+
verify(staleHandler, times(1)).execute(any(ExecutionEvent.class));
255+
}
256+
200257
@Test
201258
public void testExecuteMultiStrokeBinding() {
202259
assertFalse(twoStrokeHandler.q2);

0 commit comments

Comments
 (0)