Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: d046f7834c30321fb34e9423340295e1ff4efc68 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
/*******************************************************************************
 * Copyright (c) 2009, 2013 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 ******************************************************************************/

package org.eclipse.e4.ui.bindings.keys;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import javax.inject.Inject;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.IHandler;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.CommandException;
import org.eclipse.e4.core.commands.EHandlerService;
import org.eclipse.e4.core.commands.internal.HandlerServiceImpl;
import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.core.services.log.Logger;
import org.eclipse.e4.ui.bindings.EBindingService;
import org.eclipse.e4.ui.bindings.internal.KeyAssistDialog;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.bindings.keys.ParseException;
import org.eclipse.jface.bindings.keys.SWTKeySupport;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Widget;

/**
 * <p>
 * Controls the keyboard input into the workbench key binding architecture. This allows key events
 * to be programmatically pushed into the key binding architecture -- potentially triggering the
 * execution of commands. It is used by the <code>e4 Workbench</code> to listen for events on the
 * <code>Display</code>.
 * </p>
 */
public class KeyBindingDispatcher {

	private KeyAssistDialog keyAssistDialog = null;

	/**
	 * A display filter for handling key bindings. This filter can either be enabled or disabled. If
	 * disabled, the filter does not process incoming events. The filter starts enabled.
	 * 
	 * @since 3.1
	 */
	public final class KeyDownFilter implements Listener {

		/**
		 * Whether the filter is enabled.
		 */
		private transient boolean enabled = true;

		/**
		 * Handles an incoming traverse or key down event.
		 * 
		 * @param event
		 *            The event to process; must not be <code>null</code>.
		 */
		public final void handleEvent(final Event event) {
			if (!enabled) {
				return;
			}

			filterKeySequenceBindings(event);
		}

		/**
		 * Returns whether the key binding filter is enabled.
		 * 
		 * @return Whether the key filter is enabled.
		 */
		public final boolean isEnabled() {
			return enabled;
		}

		/**
		 * Sets whether this filter should be enabled or disabled.
		 * 
		 * @param enabled
		 *            Whether key binding filter should be enabled.
		 */
		public final void setEnabled(final boolean enabled) {
			this.enabled = enabled;
		}
	}

	/** The collection of keys that are to be processed out-of-order. */
	static KeySequence outOfOrderKeys;

	static {

		try {
			outOfOrderKeys = KeySequence.getInstance("ESC DEL"); //$NON-NLS-1$
		} catch (ParseException e) {
			outOfOrderKeys = KeySequence.getInstance();
			// String message = "Could not parse out-of-order keys definition: 'ESC DEL'.  Continuing with no out-of-order keys."; //$NON-NLS-1$
			// TODO we need to do some logging here
		}
	}

	/**
	 * Generates any key strokes that are near matches to the given event. The first such key stroke
	 * is always the exactly matching key stroke.
	 * 
	 * @param event
	 *            The event from which the key strokes should be generated; must not be
	 *            <code>null</code>.
	 * @return The set of nearly matching key strokes. It is never <code>null</code>, but may be
	 *         empty.
	 */
	public static List<KeyStroke> generatePossibleKeyStrokes(Event event) {
		final List<KeyStroke> keyStrokes = new ArrayList<KeyStroke>(3);

		/*
		 * If this is not a keyboard event, then there are no key strokes. This can happen if we are
		 * listening to focus traversal events.
		 */
		if ((event.stateMask == 0) && (event.keyCode == 0) && (event.character == 0)) {
			return keyStrokes;
		}

		// Add each unique key stroke to the list for consideration.
		final int firstAccelerator = SWTKeySupport.convertEventToUnmodifiedAccelerator(event);
		keyStrokes.add(SWTKeySupport.convertAcceleratorToKeyStroke(firstAccelerator));

		// We shouldn't allow delete to undergo shift resolution.
		if (event.character == SWT.DEL) {
			return keyStrokes;
		}

		final int secondAccelerator = SWTKeySupport
				.convertEventToUnshiftedModifiedAccelerator(event);
		if (secondAccelerator != firstAccelerator) {
			keyStrokes.add(SWTKeySupport.convertAcceleratorToKeyStroke(secondAccelerator));
		}

		final int thirdAccelerator = SWTKeySupport.convertEventToModifiedAccelerator(event);
		if ((thirdAccelerator != secondAccelerator) && (thirdAccelerator != firstAccelerator)) {
			keyStrokes.add(SWTKeySupport.convertAcceleratorToKeyStroke(thirdAccelerator));
		}

		return keyStrokes;
	}

	/**
	 * <p>
	 * Determines whether the given event represents a key press that should be handled as an
	 * out-of-order event. An out-of-order key press is one that is passed to the focus control
	 * first. Only if the focus control fails to respond will the regular key bindings get applied.
	 * </p>
	 * <p>
	 * Care must be taken in choosing which keys are chosen as out-of-order keys. This method has
	 * only been designed and test to work with the unmodified "Escape" key stroke.
	 * </p>
	 * 
	 * @param keyStrokes
	 *            The key stroke in which to look for out-of-order keys; must not be
	 *            <code>null</code>.
	 * @return <code>true</code> if the key is an out-of-order key; <code>false</code> otherwise.
	 */
	private static boolean isOutOfOrderKey(List<KeyStroke> keyStrokes) {
		// Compare to see if one of the possible key strokes is out of order.
		final KeyStroke[] outOfOrderKeyStrokes = outOfOrderKeys.getKeyStrokes();
		final int outOfOrderKeyStrokesLength = outOfOrderKeyStrokes.length;
		for (int i = 0; i < outOfOrderKeyStrokesLength; i++) {
			if (keyStrokes.contains(outOfOrderKeyStrokes[i])) {
				return true;
			}
		}
		return false;
	}

	/**
	 * The time in milliseconds to wait after pressing a key before displaying the key assist
	 * dialog.
	 */
	private static final int DELAY = 1000;

	private EBindingService bindingService;

	private IEclipseContext context;

	private EHandlerService handlerService;

	/**
	 * The listener that runs key events past the global key bindings.
	 */
	private final KeyDownFilter keyDownFilter = new KeyDownFilter();

	/**
	 * The single out-of-order listener used by the workbench. This listener is attached to one
	 * widget at a time, and is used to catch key down events after all processing is done. This
	 * technique is used so that some keys will have their native behaviour happen first.
	 * 
	 * @since 3.1
	 */
	private final OutOfOrderListener outOfOrderListener = new OutOfOrderListener(this);

	/**
	 * The single out-of-order verify listener used by the workbench. This listener is attached to
	 * one</code> StyledText</code> at a time, and is used to catch verify events after all
	 * processing is done. This technique is used so that some keys will have their native behaviour
	 * happen first.
	 * 
	 * @since 3.1
	 */
	private final OutOfOrderVerifyListener outOfOrderVerifyListener = new OutOfOrderVerifyListener(
			outOfOrderListener);

	/**
	 * The mode is the current state of the key binding architecture. In the case of multi-stroke
	 * key bindings, this can be a partially complete key binding.
	 */
	private KeySequence state = KeySequence.getInstance();

	private long startTime;

	@Inject
	@Optional
	private Logger logger;

	/**
	 * 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.
	 * Execution failures are logged. When this method completes, the key binding state is reset.
	 * 
	 * @param parameterizedCommand
	 *            The command that should be executed; should not be <code>null</code>.
	 * @param trigger
	 *            The triggering event; may be <code>null</code>.
	 * @return <code>true</code> if there was a handler; <code>false</code> otherwise.
	 * @throws CommandException
	 *             if the handler does not complete execution for some reason. It is up to the
	 *             caller of this method to decide whether to log the message, display a dialog, or
	 *             ignore this exception entirely.
	 */
	public final boolean executeCommand(final ParameterizedCommand parameterizedCommand,
			final Event trigger) throws CommandException {

		// Reset the key binding state (close window, clear status line, etc.)
		resetState(false);

		final EHandlerService handlerService = getHandlerService();
		final Command command = parameterizedCommand.getCommand();

		final IEclipseContext staticContext = EclipseContextFactory.create("keys-staticContext"); //$NON-NLS-1$
		staticContext.set(Event.class, trigger);

		final boolean commandDefined = command.isDefined();
		// boolean commandEnabled;
		boolean commandHandled = false;

		try {
			// commandEnabled = handlerService.canExecute(parameterizedCommand, staticContext);
			Object obj = HandlerServiceImpl.lookUpHandler(context, command.getId());
			if (obj != null) {
				if (obj instanceof IHandler) {
					commandHandled = ((IHandler) obj).isHandled();
				} else {
					commandHandled = true;
				}
			}

			try {
				handlerService.executeHandler(parameterizedCommand, staticContext);
			} catch (final Exception e) {
				commandHandled = false;
				if (logger != null) {
					logger.error(e);
				}
			}
			/*
			 * Now that the command has executed (and had the opportunity to use the remembered
			 * state of the dialog), it is safe to delete that information.
			 */
			if (keyAssistDialog != null) {
				keyAssistDialog.clearRememberedState();
			}
		} finally {
			staticContext.dispose();
		}
		return (commandDefined && commandHandled);
	}

	/**
	 * <p>
	 * Launches the command matching a the typed key. This filter an incoming
	 * <code>SWT.KeyDown</code> or <code>SWT.Traverse</code> event at the level of the display
	 * (i.e., before it reaches the widgets). It does not allow processing in a dialog or if the key
	 * strokes does not contain a natural key.
	 * </p>
	 * <p>
	 * Some key strokes (defined as a property) are declared as out-of-order keys. This means that
	 * they are processed by the widget <em>first</em>. Only if the other widget listeners do no
	 * useful work does it try to process key bindings. For example, "ESC" can cancel the current
	 * widget action, if there is one, without triggering key bindings.
	 * </p>
	 * 
	 * @param event
	 *            The incoming event; must not be <code>null</code>.
	 */
	private void filterKeySequenceBindings(Event event) {
		/*
		 * Only process key strokes containing natural keys to trigger key bindings.
		 */
		if ((event.keyCode & SWT.MODIFIER_MASK) != 0) {
			return;
		}

		// Allow special key out-of-order processing.
		List<KeyStroke> keyStrokes = generatePossibleKeyStrokes(event);
		if (isOutOfOrderKey(keyStrokes)) {
			Widget widget = event.widget;
			if ((event.character == SWT.DEL)
					&& ((event.stateMask & SWT.MODIFIER_MASK) == 0)
					&& ((widget instanceof Text) || (widget instanceof Combo)
							|| (widget instanceof Browser) || (widget instanceof CCombo))) {
				/*
				 * KLUDGE. Bug 54654. The text widget relies on no listener doing any work before
				 * dispatching the native delete event. This does not work, as we are restricted to
				 * listeners. However, it can be said that pressing a delete key in a text widget
				 * will never use key bindings. This can be shown be considering how the event
				 * dispatching is expected to work in a text widget. So, we should do nothing ...
				 * ever.
				 */
				return;

			} else if (widget instanceof StyledText) {

				if (event.type == SWT.KeyDown) {
					/*
					 * KLUDGE. Some people try to do useful work in verify listeners. The way verify
					 * listeners work in SWT, we need to verify the key as well; otherwise, we can't
					 * detect that useful work has been done.
					 */
					if (!outOfOrderVerifyListener.isActive(event.time)) {
						((StyledText) widget).addVerifyKeyListener(outOfOrderVerifyListener);
						outOfOrderVerifyListener.setActive(event.time);
					}
				}

			} else {
				if (!outOfOrderListener.isActive(event.time)) {
					widget.addListener(SWT.KeyDown, outOfOrderListener);
					outOfOrderListener.setActive(event.time);
				}
			}

			/*
			 * Otherwise, we count on a key down arriving eventually. Expecting out of order
			 * handling on Ctrl+Tab, for example, is a bad idea (stick to keys that are not window
			 * traversal keys).
			 */

		} else {
			processKeyEvent(keyStrokes, event);
		}
	}

	private EBindingService getBindingService() {
		if (bindingService == null) {
			bindingService = (EBindingService) context.get(EBindingService.class.getName());
		}
		return bindingService;
	}

	private EHandlerService getHandlerService() {
		if (handlerService == null) {
			handlerService = (EHandlerService) context.get(EHandlerService.class.getName());
		}
		return handlerService;
	}

	private Display getDisplay() {
		return Display.getCurrent();
	}

	/**
	 * An accessor for the filter that processes key down and traverse events on the display.
	 * 
	 * @return The global key down and traverse filter; never <code>null</code>.
	 */
	public KeyDownFilter getKeyDownFilter() {
		return keyDownFilter;
	}

	/**
	 * Determines whether the key sequence is a perfect match for any command. If there is a match,
	 * then the corresponding command identifier is returned.
	 * 
	 * @param keySequence
	 *            The key sequence to check for a match; must never be <code>null</code>.
	 * @return The perfectly matching command; <code>null</code> if no command matches.
	 */
	private ParameterizedCommand getPerfectMatch(KeySequence keySequence) {
		Binding perfectMatch = getBindingService().getPerfectMatch(keySequence);
		return perfectMatch == null ? null : perfectMatch.getParameterizedCommand();
	}

	/**
	 * Changes the key binding state to the given value. This should be an incremental change, but
	 * there are no checks to guarantee this is so. It also sets up a <code>Shell</code> to be
	 * displayed after one second has elapsed. This shell will show the user the possible
	 * completions for what they have typed.
	 * 
	 * @param sequence
	 *            The new key sequence for the state; should not be <code>null</code>.
	 */
	private void incrementState(final KeySequence sequence) {
		state = sequence;
		// Record the starting time.
		startTime = System.currentTimeMillis();
		final long myStartTime = startTime;
		final Display display = getDisplay();
		display.timerExec(DELAY, new Runnable() {
			public void run() {
				if ((System.currentTimeMillis() > (myStartTime - DELAY))
						&& (startTime == myStartTime)) {
					Collection<Binding> partialMatches = bindingService.getPartialMatches(sequence);
					openKeyAssistShell(partialMatches);
				}
			}
		});

	}

	/**
	 * Opens a <code>KeyAssistDialog</code> to assist the user in completing a multi-stroke key
	 * binding. This method lazily creates a <code>keyAssistDialog</code> and shares it between
	 * executions.
	 */
	private final void openKeyAssistShell(final Collection<Binding> bindings) {
		if (keyAssistDialog == null) {
			keyAssistDialog = new KeyAssistDialog(context, this);
		}
		if (keyAssistDialog.getShell() == null) {
			keyAssistDialog.setParentShell(getDisplay().getActiveShell());
		}
		keyAssistDialog.open(bindings);
	}

	/**
	 * Determines whether the key sequence partially matches on of the active key bindings.
	 * 
	 * @param keySequence
	 *            The key sequence to check for a partial match; must never be <code>null</code>.
	 * @return <code>true</code> if there is a partial match; <code>false</code> otherwise.
	 */
	private boolean isPartialMatch(KeySequence keySequence) {
		return getBindingService().isPartialMatch(keySequence);
	}

	/**
	 * Determines whether the key sequence perfectly matches on of the active key bindings.
	 * 
	 * @param keySequence
	 *            The key sequence to check for a perfect match; must never be <code>null</code>.
	 * @return <code>true</code> if there is a perfect match; <code>false</code> otherwise.
	 */
	private boolean isPerfectMatch(KeySequence keySequence) {
		return getBindingService().isPerfectMatch(keySequence);
	}

	/**
	 * @param potentialKeyStrokes
	 * @param event
	 * @return
	 */
	public boolean press(List<KeyStroke> potentialKeyStrokes, Event event) {
		KeySequence errorSequence = null;
		Collection<Binding> errorMatch = null;

		KeySequence sequenceBeforeKeyStroke = state;
		for (Iterator<KeyStroke> iterator = potentialKeyStrokes.iterator(); iterator.hasNext();) {
			KeySequence sequenceAfterKeyStroke = KeySequence.getInstance(sequenceBeforeKeyStroke,
					iterator.next());
			if (isPartialMatch(sequenceAfterKeyStroke)) {
				incrementState(sequenceAfterKeyStroke);
				return true;

			} else if (isPerfectMatch(sequenceAfterKeyStroke)) {
				final ParameterizedCommand cmd = getPerfectMatch(sequenceAfterKeyStroke);
				try {
					return executeCommand(cmd, event) || !sequenceBeforeKeyStroke.isEmpty();
				} catch (final CommandException e) {
					return true;
				}

			} else if ((keyAssistDialog != null)
					&& (keyAssistDialog.getShell() != null)
					&& ((event.keyCode == SWT.ARROW_DOWN) || (event.keyCode == SWT.ARROW_UP)
							|| (event.keyCode == SWT.ARROW_LEFT)
							|| (event.keyCode == SWT.ARROW_RIGHT) || (event.keyCode == SWT.CR)
							|| (event.keyCode == SWT.PAGE_UP) || (event.keyCode == SWT.PAGE_DOWN))) {
				// We don't want to swallow keyboard navigation keys.
				return false;

			} else {
				Collection<Binding> matches = getBindingService().getConflictsFor(
						sequenceAfterKeyStroke);
				if (matches != null && !matches.isEmpty()) {
					errorSequence = sequenceAfterKeyStroke;
					errorMatch = matches;
				}
			}
		}
		resetState(true);
		if (sequenceBeforeKeyStroke.isEmpty() && errorSequence != null) {
			openKeyAssistShell(errorMatch);
		}
		return !sequenceBeforeKeyStroke.isEmpty();
	}

	/**
	 * <p>
	 * Actually performs the processing of the key event by interacting with the
	 * <code>ICommandManager</code>. If work is carried out, then the event is stopped here (i.e.,
	 * <code>event.doit = false</code>). It does not do any processing if there are no matching key
	 * strokes.
	 * </p>
	 * <p>
	 * If the active <code>Shell</code> is not the same as the one to which the state is associated,
	 * then a reset occurs.
	 * </p>
	 * 
	 * @param keyStrokes
	 *            The set of all possible matching key strokes; must not be <code>null</code>.
	 * @param event
	 *            The event to process; must not be <code>null</code>.
	 */
	void processKeyEvent(List<KeyStroke> keyStrokes, Event event) {
		// Dispatch the keyboard shortcut, if any.
		boolean eatKey = false;
		if (!keyStrokes.isEmpty()) {
			eatKey = press(keyStrokes, event);
		}

		if (eatKey) {
			switch (event.type) {
			case SWT.KeyDown:
				event.doit = false;
				break;
			case SWT.Traverse:
				event.detail = SWT.TRAVERSE_NONE;
				event.doit = true;
				break;
			default:
			}
			event.type = SWT.NONE;
		}
	}

	private void resetState(boolean clearRememberedState) {
		startTime = Long.MAX_VALUE;
		state = KeySequence.getInstance();
		closeMultiKeyAssistShell();
		if (keyAssistDialog != null && clearRememberedState) {
			keyAssistDialog.clearRememberedState();
		}
	}

	final public KeySequence getBuffer() {
		return state;
	}

	@Inject
	public void setContext(IEclipseContext context) {
		this.context = context;
	}

	/**
	 * Closes the multi-stroke key binding assistant shell, if it exists and isn't already disposed.
	 */
	private void closeMultiKeyAssistShell() {
		if (keyAssistDialog != null) {
			final Shell shell = keyAssistDialog.getShell();
			if ((shell != null) && (!shell.isDisposed()) && (shell.isVisible())) {
				keyAssistDialog.close(true);
			}
		}
	}

}

Back to the top