001: /*******************************************************************************
002: * Copyright (c) 2000, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.ui.internal.keys;
011:
012: import java.util.ArrayList;
013: import java.util.Collection;
014: import java.util.Iterator;
015: import java.util.List;
016: import java.util.ResourceBundle;
017:
018: import org.eclipse.core.commands.Command;
019: import org.eclipse.core.commands.NotEnabledException;
020: import org.eclipse.core.commands.NotHandledException;
021: import org.eclipse.core.commands.ParameterizedCommand;
022: import org.eclipse.core.commands.common.CommandException;
023: import org.eclipse.core.commands.common.NotDefinedException;
024: import org.eclipse.core.commands.util.Tracing;
025: import org.eclipse.core.runtime.IStatus;
026: import org.eclipse.core.runtime.Status;
027: import org.eclipse.jface.bindings.Binding;
028: import org.eclipse.jface.bindings.keys.KeySequence;
029: import org.eclipse.jface.bindings.keys.KeyStroke;
030: import org.eclipse.jface.bindings.keys.ParseException;
031: import org.eclipse.jface.bindings.keys.SWTKeySupport;
032: import org.eclipse.jface.internal.InternalPolicy;
033: import org.eclipse.swt.SWT;
034: import org.eclipse.swt.custom.StyledText;
035: import org.eclipse.swt.widgets.Combo;
036: import org.eclipse.swt.widgets.Control;
037: import org.eclipse.swt.widgets.Display;
038: import org.eclipse.swt.widgets.Event;
039: import org.eclipse.swt.widgets.Listener;
040: import org.eclipse.swt.widgets.Shell;
041: import org.eclipse.swt.widgets.Text;
042: import org.eclipse.swt.widgets.Widget;
043: import org.eclipse.ui.IWindowListener;
044: import org.eclipse.ui.IWorkbench;
045: import org.eclipse.ui.IWorkbenchWindow;
046: import org.eclipse.ui.contexts.IContextService;
047: import org.eclipse.ui.handlers.IHandlerService;
048: import org.eclipse.ui.internal.Workbench;
049: import org.eclipse.ui.internal.WorkbenchPlugin;
050: import org.eclipse.ui.internal.contexts.ContextService;
051: import org.eclipse.ui.internal.handlers.HandlerService;
052: import org.eclipse.ui.internal.misc.Policy;
053: import org.eclipse.ui.internal.misc.StatusUtil;
054: import org.eclipse.ui.internal.util.Util;
055: import org.eclipse.ui.keys.IBindingService;
056: import org.eclipse.ui.statushandlers.StatusManager;
057:
058: import com.ibm.icu.text.MessageFormat;
059:
060: /**
061: * <p>
062: * Controls the keyboard input into the workbench key binding architecture. This
063: * allows key events to be programmatically pushed into the key binding
064: * architecture -- potentially triggering the execution of commands. It is used
065: * by the <code>Workbench</code> to listen for events on the
066: * <code>Display</code>.
067: * </p>
068: * <p>
069: * This class is not designed to be thread-safe. It is assumed that all access
070: * to the <code>press</code> method is done through the event loop. Accessing
071: * this method outside the event loop can cause corruption of internal state.
072: * </p>
073: *
074: * @since 3.0
075: */
076: public final class WorkbenchKeyboard {
077:
078: /**
079: * A display filter for handling key bindings. This filter can either be
080: * enabled or disabled. If disabled, the filter does not process incoming
081: * events. The filter starts enabled.
082: *
083: * @since 3.1
084: */
085: public final class KeyDownFilter implements Listener {
086:
087: /**
088: * Whether the filter is enabled.
089: */
090: private transient boolean enabled = true;
091:
092: /**
093: * Handles an incoming traverse or key down event.
094: *
095: * @param event
096: * The event to process; must not be <code>null</code>.
097: */
098: public final void handleEvent(final Event event) {
099: if (!enabled) {
100: return;
101: }
102:
103: if (DEBUG && DEBUG_VERBOSE) {
104: final StringBuffer buffer = new StringBuffer(
105: "Listener.handleEvent(type = "); //$NON-NLS-1$
106: switch (event.type) {
107: case SWT.KeyDown:
108: buffer.append("KeyDown"); //$NON-NLS-1$
109: break;
110: case SWT.Traverse:
111: buffer.append("Traverse"); //$NON-NLS-1$
112: break;
113: default:
114: buffer.append(event.type);
115: }
116: buffer.append(", stateMask = 0x" //$NON-NLS-1$
117: + Integer.toHexString(event.stateMask)
118: + ", keyCode = 0x" //$NON-NLS-1$
119: + Integer.toHexString(event.keyCode)
120: + ", time = " //$NON-NLS-1$
121: + event.time + ", character = 0x" //$NON-NLS-1$
122: + Integer.toHexString(event.character) + ")"); //$NON-NLS-1$
123: Tracing.printTrace("KEYS", buffer.toString()); //$NON-NLS-1$
124: }
125:
126: filterKeySequenceBindings(event);
127: }
128:
129: /**
130: * Returns whether the key binding filter is enabled.
131: *
132: * @return Whether the key filter is enabled.
133: */
134: public final boolean isEnabled() {
135: return enabled;
136: }
137:
138: /**
139: * Sets whether this filter should be enabled or disabled.
140: *
141: * @param enabled
142: * Whether key binding filter should be enabled.
143: */
144: public final void setEnabled(final boolean enabled) {
145: this .enabled = enabled;
146: }
147: }
148:
149: /**
150: * Whether the keyboard should kick into debugging mode. This causes real
151: * key bindings trapped by the key binding architecture to be reported.
152: */
153: private static final boolean DEBUG = Policy.DEBUG_KEY_BINDINGS;
154:
155: /**
156: * Whether the keyboard should report every event received by its global
157: * filter.
158: */
159: private static final boolean DEBUG_VERBOSE = Policy.DEBUG_KEY_BINDINGS_VERBOSE;
160:
161: /**
162: * The time in milliseconds to wait after pressing a key before displaying
163: * the key assist dialog.
164: */
165: private static final int DELAY = 1000;
166:
167: /** The collection of keys that are to be processed out-of-order. */
168: static KeySequence outOfOrderKeys;
169:
170: /**
171: * The translation bundle in which to look up internationalized text.
172: */
173: private final static ResourceBundle RESOURCE_BUNDLE = ResourceBundle
174: .getBundle(WorkbenchKeyboard.class.getName());
175:
176: static {
177:
178: try {
179: outOfOrderKeys = KeySequence.getInstance("ESC DEL"); //$NON-NLS-1$
180: } catch (ParseException e) {
181: outOfOrderKeys = KeySequence.getInstance();
182: String message = "Could not parse out-of-order keys definition: 'ESC DEL'. Continuing with no out-of-order keys."; //$NON-NLS-1$
183: WorkbenchPlugin.log(message, new Status(IStatus.ERROR,
184: WorkbenchPlugin.PI_WORKBENCH, 0, message, e));
185: }
186: }
187:
188: /**
189: * Generates any key strokes that are near matches to the given event. The
190: * first such key stroke is always the exactly matching key stroke.
191: *
192: * @param event
193: * The event from which the key strokes should be generated; must
194: * not be <code>null</code>.
195: * @return The set of nearly matching key strokes. It is never
196: * <code>null</code>, but may be empty.
197: */
198: public static List generatePossibleKeyStrokes(Event event) {
199: final List keyStrokes = new ArrayList(3);
200:
201: /*
202: * If this is not a keyboard event, then there are no key strokes. This
203: * can happen if we are listening to focus traversal events.
204: */
205: if ((event.stateMask == 0) && (event.keyCode == 0)
206: && (event.character == 0)) {
207: return keyStrokes;
208: }
209:
210: // Add each unique key stroke to the list for consideration.
211: final int firstAccelerator = SWTKeySupport
212: .convertEventToUnmodifiedAccelerator(event);
213: keyStrokes.add(SWTKeySupport
214: .convertAcceleratorToKeyStroke(firstAccelerator));
215:
216: // We shouldn't allow delete to undergo shift resolution.
217: if (event.character == SWT.DEL) {
218: return keyStrokes;
219: }
220:
221: final int secondAccelerator = SWTKeySupport
222: .convertEventToUnshiftedModifiedAccelerator(event);
223: if (secondAccelerator != firstAccelerator) {
224: keyStrokes.add(SWTKeySupport
225: .convertAcceleratorToKeyStroke(secondAccelerator));
226: }
227:
228: final int thirdAccelerator = SWTKeySupport
229: .convertEventToModifiedAccelerator(event);
230: if ((thirdAccelerator != secondAccelerator)
231: && (thirdAccelerator != firstAccelerator)) {
232: keyStrokes.add(SWTKeySupport
233: .convertAcceleratorToKeyStroke(thirdAccelerator));
234: }
235:
236: return keyStrokes;
237: }
238:
239: /**
240: * <p>
241: * Determines whether the given event represents a key press that should be
242: * handled as an out-of-order event. An out-of-order key press is one that
243: * is passed to the focus control first. Only if the focus control fails to
244: * respond will the regular key bindings get applied.
245: * </p>
246: * <p>
247: * Care must be taken in choosing which keys are chosen as out-of-order
248: * keys. This method has only been designed and test to work with the
249: * unmodified "Escape" key stroke.
250: * </p>
251: *
252: * @param keyStrokes
253: * The key stroke in which to look for out-of-order keys; must
254: * not be <code>null</code>.
255: * @return <code>true</code> if the key is an out-of-order key;
256: * <code>false</code> otherwise.
257: */
258: private static boolean isOutOfOrderKey(List keyStrokes) {
259: // Compare to see if one of the possible key strokes is out of order.
260: final KeyStroke[] outOfOrderKeyStrokes = outOfOrderKeys
261: .getKeyStrokes();
262: final int outOfOrderKeyStrokesLength = outOfOrderKeyStrokes.length;
263: for (int i = 0; i < outOfOrderKeyStrokesLength; i++) {
264: if (keyStrokes.contains(outOfOrderKeyStrokes[i])) {
265: return true;
266: }
267: }
268: return false;
269: }
270:
271: /**
272: * The binding manager to be used to resolve key bindings. This member
273: * variable will be <code>null</code> if it has not yet been initialized.
274: */
275: private IBindingService bindingService = null;
276:
277: /**
278: * The <code>KeyAssistDialog</code> displayed to the user to assist them
279: * in completing a multi-stroke keyboard shortcut.
280: *
281: * @since 3.1
282: */
283: private KeyAssistDialog keyAssistDialog = null;
284:
285: /**
286: * The listener that runs key events past the global key bindings.
287: */
288: private final KeyDownFilter keyDownFilter = new KeyDownFilter();
289:
290: /**
291: * The single out-of-order listener used by the workbench. This listener is
292: * attached to one widget at a time, and is used to catch key down events
293: * after all processing is done. This technique is used so that some keys
294: * will have their native behaviour happen first.
295: *
296: * @since 3.1
297: */
298: private final OutOfOrderListener outOfOrderListener = new OutOfOrderListener(
299: this );
300:
301: /**
302: * The single out-of-order verify listener used by the workbench. This
303: * listener is attached to one</code> StyledText</code> at a time, and is
304: * used to catch verify events after all processing is done. This technique
305: * is used so that some keys will have their native behaviour happen first.
306: *
307: * @since 3.1
308: */
309: private final OutOfOrderVerifyListener outOfOrderVerifyListener = new OutOfOrderVerifyListener(
310: outOfOrderListener);
311:
312: /**
313: * The time at which the last timer was started. This is used to judge if a
314: * sufficient amount of time has elapsed. This is simply the output of
315: * <code>System.currentTimeMillis()</code>.
316: */
317: private long startTime = Long.MAX_VALUE;
318:
319: /**
320: * The mode is the current state of the key binding architecture. In the
321: * case of multi-stroke key bindings, this can be a partially complete key
322: * binding.
323: */
324: private final KeyBindingState state;
325:
326: /**
327: * The window listener responsible for maintaining internal state as the
328: * focus moves between windows on the desktop.
329: */
330: private final IWindowListener windowListener = new IWindowListener() {
331:
332: /*
333: * (non-Javadoc)
334: *
335: * @see org.eclipse.ui.IWindowListener#windowActivated(org.eclipse.ui.IWorkbenchWindow)
336: */
337: public void windowActivated(IWorkbenchWindow window) {
338: checkActiveWindow(window);
339: }
340:
341: /*
342: * (non-Javadoc)
343: *
344: * @see org.eclipse.ui.IWindowListener#windowClosed(org.eclipse.ui.IWorkbenchWindow)
345: */
346: public void windowClosed(IWorkbenchWindow window) {
347: // Do nothing.
348: }
349:
350: /*
351: * (non-Javadoc)
352: *
353: * @see org.eclipse.ui.IWindowListener#windowDeactivated(org.eclipse.ui.IWorkbenchWindow)
354: */
355: public void windowDeactivated(IWorkbenchWindow window) {
356: // Do nothing
357: }
358:
359: /*
360: * (non-Javadoc)
361: *
362: * @see org.eclipse.ui.IWindowListener#windowOpened(org.eclipse.ui.IWorkbenchWindow)
363: */
364: public void windowOpened(IWorkbenchWindow window) {
365: // Do nothing.
366: }
367: };
368:
369: /**
370: * The workbench on which this keyboard interface should act.
371: */
372: private final IWorkbench workbench;
373:
374: /**
375: * Constructs a new instance of <code>WorkbenchKeyboard</code> associated
376: * with a particular workbench.
377: *
378: * @param associatedWorkbench
379: * The workbench with which this keyboard interface should work;
380: * must not be <code>null</code>.
381: * @since 3.1
382: */
383: public WorkbenchKeyboard(Workbench associatedWorkbench) {
384: workbench = associatedWorkbench;
385: state = new KeyBindingState(associatedWorkbench);
386: workbench.addWindowListener(windowListener);
387: }
388:
389: /**
390: * Verifies that the active workbench window is the same as the workbench
391: * window associated with the state. This is used to verify that the state
392: * is properly reset as focus changes. When they are not the same, the state
393: * is reset and associated with the newly activated window.
394: *
395: * @param window
396: * The activated window; must not be <code>null</code>.
397: */
398: private void checkActiveWindow(IWorkbenchWindow window) {
399: if (!window.equals(state.getAssociatedWindow())) {
400: resetState(true);
401: state.setAssociatedWindow(window);
402: }
403: }
404:
405: /**
406: * Closes the multi-stroke key binding assistant shell, if it exists and
407: * isn't already disposed.
408: */
409: private void closeMultiKeyAssistShell() {
410: if (keyAssistDialog != null) {
411: final Shell shell = keyAssistDialog.getShell();
412: if ((shell != null) && (!shell.isDisposed())
413: && (shell.isVisible())) {
414: keyAssistDialog.close(true);
415: }
416: }
417: }
418:
419: /**
420: * Performs the actual execution of the command by looking up the current
421: * handler from the command manager. If there is a handler and it is
422: * enabled, then it tries the actual execution. Execution failures are
423: * logged. When this method completes, the key binding state is reset.
424: *
425: * @param binding
426: * The binding that should be executed; should not be
427: * <code>null</code>.
428: * @param trigger
429: * The triggering event; may be <code>null</code>.
430: * @return <code>true</code> if there was a handler; <code>false</code>
431: * otherwise.
432: * @throws CommandException
433: * if the handler does not complete execution for some reason.
434: * It is up to the caller of this method to decide whether to
435: * log the message, display a dialog, or ignore this exception
436: * entirely.
437: */
438: final boolean executeCommand(final Binding binding,
439: final Event trigger) throws CommandException {
440: final ParameterizedCommand parameterizedCommand = binding
441: .getParameterizedCommand();
442:
443: if (DEBUG) {
444: Tracing.printTrace("KEYS", //$NON-NLS-1$
445: "WorkbenchKeyboard.executeCommand(commandId = '" //$NON-NLS-1$
446: + parameterizedCommand.getId()
447: + "', parameters = " //$NON-NLS-1$
448: + parameterizedCommand.getParameterMap()
449: + ')');
450: }
451:
452: // Reset the key binding state (close window, clear status line, etc.)
453: resetState(false);
454:
455: // Dispatch to the handler.
456: final Command command = parameterizedCommand.getCommand();
457: final boolean commandDefined = command.isDefined();
458: final boolean commandHandled = command.isHandled();
459: final boolean commandEnabled = command.isEnabled();
460:
461: if (DEBUG && DEBUG_VERBOSE) {
462: if (!commandDefined) {
463: Tracing.printTrace("KEYS", " not defined"); //$NON-NLS-1$ //$NON-NLS-2$
464: } else if (!commandHandled) {
465: Tracing.printTrace("KEYS", " not handled"); //$NON-NLS-1$ //$NON-NLS-2$
466: } else if (!commandEnabled) {
467: Tracing.printTrace("KEYS", " not enabled"); //$NON-NLS-1$ //$NON-NLS-2$
468: }
469: }
470:
471: try {
472: final IHandlerService handlerService = (IHandlerService) workbench
473: .getService(IHandlerService.class);
474: handlerService
475: .executeCommand(parameterizedCommand, trigger);
476: } catch (final NotDefinedException e) {
477: // The command is not defined. Forwarded to the IExecutionListener.
478: } catch (final NotEnabledException e) {
479: // The command is not enabled. Forwarded to the IExecutionListener.
480: } catch (final NotHandledException e) {
481: // There is no handler. Forwarded to the IExecutionListener.
482: }
483:
484: /*
485: * Now that the command has executed (and had the opportunity to use the
486: * remembered state of the dialog), it is safe to delete that
487: * information.
488: */
489: if (keyAssistDialog != null) {
490: keyAssistDialog.clearRememberedState();
491: }
492:
493: return (commandDefined && commandHandled);
494: }
495:
496: /**
497: * <p>
498: * Launches the command matching a the typed key. This filter an incoming
499: * <code>SWT.KeyDown</code> or <code>SWT.Traverse</code> event at the
500: * level of the display (i.e., before it reaches the widgets). It does not
501: * allow processing in a dialog or if the key strokes does not contain a
502: * natural key.
503: * </p>
504: * <p>
505: * Some key strokes (defined as a property) are declared as out-of-order
506: * keys. This means that they are processed by the widget <em>first</em>.
507: * Only if the other widget listeners do no useful work does it try to
508: * process key bindings. For example, "ESC" can cancel the current widget
509: * action, if there is one, without triggering key bindings.
510: * </p>
511: *
512: * @param event
513: * The incoming event; must not be <code>null</code>.
514: */
515: private void filterKeySequenceBindings(Event event) {
516: /*
517: * Only process key strokes containing natural keys to trigger key
518: * bindings.
519: */
520: if ((event.keyCode & SWT.MODIFIER_MASK) != 0) {
521: return;
522: }
523:
524: // Allow special key out-of-order processing.
525: List keyStrokes = generatePossibleKeyStrokes(event);
526: if (isOutOfOrderKey(keyStrokes)) {
527: Widget widget = event.widget;
528: if ((event.character == SWT.DEL)
529: && ((event.stateMask & SWT.MODIFIER_MASK) == 0)
530: && ((widget instanceof Text) || (widget instanceof Combo))) {
531: /*
532: * KLUDGE. Bug 54654. The text widget relies on no listener
533: * doing any work before dispatching the native delete event.
534: * This does not work, as we are restricted to listeners.
535: * However, it can be said that pressing a delete key in a text
536: * widget will never use key bindings. This can be shown be
537: * considering how the event dispatching is expected to work in
538: * a text widget. So, we should do nothing ... ever.
539: */
540: return;
541:
542: } else if (widget instanceof StyledText) {
543:
544: if (event.type == SWT.KeyDown) {
545: /*
546: * KLUDGE. Some people try to do useful work in verify
547: * listeners. The way verify listeners work in SWT, we need
548: * to verify the key as well; otherwise, we can't detect
549: * that useful work has been done.
550: */
551: if (!outOfOrderVerifyListener.isActive(event.time)) {
552: ((StyledText) widget)
553: .addVerifyKeyListener(outOfOrderVerifyListener);
554: outOfOrderVerifyListener.setActive(event.time);
555: }
556: }
557:
558: } else {
559: if (!outOfOrderListener.isActive(event.time)) {
560: widget.addListener(SWT.KeyDown, outOfOrderListener);
561: outOfOrderListener.setActive(event.time);
562: }
563:
564: }
565:
566: /*
567: * Otherwise, we count on a key down arriving eventually. Expecting
568: * out of order handling on Ctrl+Tab, for example, is a bad idea
569: * (stick to keys that are not window traversal keys).
570: */
571:
572: } else {
573: processKeyEvent(keyStrokes, event);
574:
575: }
576: }
577:
578: /**
579: * An accessor for the filter that processes key down and traverse events on
580: * the display.
581: *
582: * @return The global key down and traverse filter; never <code>null</code>.
583: */
584: public KeyDownFilter getKeyDownFilter() {
585: return keyDownFilter;
586: }
587:
588: /**
589: * Determines whether the key sequence is a perfect match for any command.
590: * If there is a match, then the corresponding command identifier is
591: * returned.
592: *
593: * @param keySequence
594: * The key sequence to check for a match; must never be
595: * <code>null</code>.
596: * @return The binding for the perfectly matching command; <code>null</code>
597: * if no command matches.
598: */
599: private Binding getPerfectMatch(KeySequence keySequence) {
600: if (bindingService == null) {
601: bindingService = (IBindingService) workbench
602: .getService(IBindingService.class);
603: }
604: return bindingService.getPerfectMatch(keySequence);
605: }
606:
607: final KeySequence getBuffer() {
608: return state.getCurrentSequence();
609: }
610:
611: /**
612: * Changes the key binding state to the given value. This should be an
613: * incremental change, but there are no checks to guarantee this is so. It
614: * also sets up a <code>Shell</code> to be displayed after one second has
615: * elapsed. This shell will show the user the possible completions for what
616: * they have typed.
617: *
618: * @param sequence
619: * The new key sequence for the state; should not be
620: * <code>null</code>.
621: */
622: private void incrementState(KeySequence sequence) {
623: // Record the starting time.
624: startTime = System.currentTimeMillis();
625: final long myStartTime = startTime;
626:
627: // Update the state.
628: state.setCurrentSequence(sequence);
629: state.setAssociatedWindow(workbench.getActiveWorkbenchWindow());
630:
631: // After some time, open a shell displaying the possible completions.
632: final Display display = workbench.getDisplay();
633: display.timerExec(DELAY, new Runnable() {
634: public void run() {
635: if ((System.currentTimeMillis() > (myStartTime - DELAY))
636: && (startTime == myStartTime)) {
637: openMultiKeyAssistShell();
638: }
639: }
640: });
641: }
642:
643: /**
644: * Determines whether the key sequence partially matches on of the active
645: * key bindings.
646: *
647: * @param keySequence
648: * The key sequence to check for a partial match; must never be
649: * <code>null</code>.
650: * @return <code>true</code> if there is a partial match;
651: * <code>false</code> otherwise.
652: */
653: private boolean isPartialMatch(KeySequence keySequence) {
654: if (bindingService == null) {
655: bindingService = (IBindingService) workbench
656: .getService(IBindingService.class);
657: }
658: return bindingService.isPartialMatch(keySequence);
659: }
660:
661: /**
662: * Determines whether the key sequence perfectly matches on of the active
663: * key bindings.
664: *
665: * @param keySequence
666: * The key sequence to check for a perfect match; must never be
667: * <code>null</code>.
668: * @return <code>true</code> if there is a perfect match;
669: * <code>false</code> otherwise.
670: */
671: private boolean isPerfectMatch(KeySequence keySequence) {
672: if (bindingService == null) {
673: bindingService = (IBindingService) workbench
674: .getService(IBindingService.class);
675: }
676: return bindingService.isPerfectMatch(keySequence);
677: }
678:
679: /**
680: * Logs the given exception, and opens a dialog explaining the failure.
681: *
682: * @param e
683: * The exception to log; must not be <code>null</code>.
684: * @param command
685: * The parameterized command for the binding to execute; may be
686: * <code>null</code>.
687: */
688: final void logException(final CommandException e,
689: final ParameterizedCommand command) {
690: Throwable nestedException = e.getCause();
691: Throwable exception = (nestedException == null) ? e
692: : nestedException;
693:
694: // If we can, include the command name in the exception.
695: String message = null;
696: if (command != null) {
697: try {
698: final String name = command.getCommand().getName();
699: message = MessageFormat.format(Util.translateString(
700: RESOURCE_BUNDLE,
701: "ExecutionError.MessageCommandName"), //$NON-NLS-1$
702: new Object[] { name });
703: } catch (final NotDefinedException nde) {
704: // Fall through (message == null)
705: }
706: }
707: if (message == null) {
708: message = Util.translateString(RESOURCE_BUNDLE,
709: "ExecutionError.Message"); //$NON-NLS-1$
710: }
711:
712: String exceptionMessage = exception.getMessage();
713: if (exceptionMessage == null) {
714: exceptionMessage = exception.getClass().getName();
715: }
716: IStatus status = new Status(IStatus.ERROR,
717: WorkbenchPlugin.PI_WORKBENCH, 0, exceptionMessage,
718: exception);
719: WorkbenchPlugin.log(message, status);
720: StatusUtil.handleStatus(message, exception, StatusManager.SHOW);
721: }
722:
723: /**
724: * Opens a <code>KeyAssistDialog</code> to assist the user in completing a
725: * multi-stroke key binding. This method lazily creates a
726: * <code>keyAssistDialog</code> and shares it between executions.
727: */
728: public final void openMultiKeyAssistShell() {
729: if (keyAssistDialog == null) {
730: keyAssistDialog = new KeyAssistDialog(workbench, this ,
731: state);
732: }
733: if (keyAssistDialog.getShell() == null) {
734: keyAssistDialog.setParentShell(Util.getShellToParentOn());
735: }
736: keyAssistDialog.open();
737: }
738:
739: /**
740: * Opens the key assist dialog to offer the user the choice of a binding to
741: * pick from the collection of bindings.
742: *
743: * @param bindings
744: * a collection of Binding objects
745: * @since 3.3
746: */
747: public final void openKeyAssistShell(final Collection bindings) {
748: if (keyAssistDialog == null) {
749: keyAssistDialog = new KeyAssistDialog(workbench,
750: WorkbenchKeyboard.this , state);
751: }
752: if (keyAssistDialog.getShell() == null) {
753: keyAssistDialog.setParentShell(Util.getShellToParentOn());
754: }
755: keyAssistDialog.open(bindings);
756: }
757:
758: /**
759: * Processes a key press with respect to the key binding architecture. This
760: * updates the mode of the command manager, and runs the current handler for
761: * the command that matches the key sequence, if any.
762: *
763: * @param potentialKeyStrokes
764: * The key strokes that could potentially match, in the order of
765: * priority; must not be <code>null</code>.
766: * @param event
767: * The event; may be <code>null</code>.
768: * @return <code>true</code> if a command is executed; <code>false</code>
769: * otherwise.
770: */
771: public boolean press(List potentialKeyStrokes, Event event) {
772: if (DEBUG && DEBUG_VERBOSE) {
773: Tracing.printTrace("KEYS", //$NON-NLS-1$
774: "WorkbenchKeyboard.press(potentialKeyStrokes = " //$NON-NLS-1$
775: + potentialKeyStrokes + ')');
776: }
777:
778: /*
779: * KLUDGE. This works around a couple of specific problems in how GTK+
780: * works. The first problem is the ordering of key press events with
781: * respect to shell activation events. If on the event thread a dialog
782: * is about to open, and the user presses a key, the key press event
783: * will arrive before the shell activation event. From the perspective
784: * of Eclipse, this means that things like two "Open Type" dialogs can
785: * appear if "Ctrl+Shift+T" is pressed twice rapidly. For more
786: * information, please see Bug 95792. The second problem is simply a bug
787: * in GTK+, for which an incomplete workaround currently exists in SWT.
788: * This makes shell activation events unreliable. Please see Bug 56231
789: * and Bug 95222 for more information.
790: */
791: if ("gtk".equals(SWT.getPlatform())) { //$NON-NLS-1$
792: final Widget widget = event.widget;
793:
794: // Update the contexts.
795: final ContextService contextService = (ContextService) workbench
796: .getService(IContextService.class);
797: if ((widget instanceof Control) && (!widget.isDisposed())) {
798: final Shell shell = ((Control) widget).getShell();
799: contextService.updateShellKludge(shell);
800: } else {
801: contextService.updateShellKludge();
802: }
803:
804: // Update the handlers.
805: final HandlerService handlerService = (HandlerService) workbench
806: .getService(IHandlerService.class);
807: if ((widget instanceof Control) && (!widget.isDisposed())) {
808: final Shell shell = ((Control) widget).getShell();
809: handlerService.updateShellKludge(shell);
810: } else {
811: handlerService.updateShellKludge();
812: }
813: }
814:
815: KeySequence errorSequence = null;
816: Collection errorMatch = null;
817:
818: KeySequence sequenceBeforeKeyStroke = state
819: .getCurrentSequence();
820: for (Iterator iterator = potentialKeyStrokes.iterator(); iterator
821: .hasNext();) {
822: KeySequence sequenceAfterKeyStroke = KeySequence
823: .getInstance(sequenceBeforeKeyStroke,
824: (KeyStroke) iterator.next());
825: if (isPartialMatch(sequenceAfterKeyStroke)) {
826: incrementState(sequenceAfterKeyStroke);
827: return true;
828:
829: } else if (isPerfectMatch(sequenceAfterKeyStroke)) {
830: final Binding binding = getPerfectMatch(sequenceAfterKeyStroke);
831: try {
832: return executeCommand(binding, event)
833: || !sequenceBeforeKeyStroke.isEmpty();
834: } catch (final CommandException e) {
835: logException(e, binding.getParameterizedCommand());
836: return true;
837: }
838:
839: } else if ((keyAssistDialog != null)
840: && (keyAssistDialog.getShell() != null)
841: && ((event.keyCode == SWT.ARROW_DOWN)
842: || (event.keyCode == SWT.ARROW_UP)
843: || (event.keyCode == SWT.ARROW_LEFT)
844: || (event.keyCode == SWT.ARROW_RIGHT)
845: || (event.keyCode == SWT.CR)
846: || (event.keyCode == SWT.PAGE_UP) || (event.keyCode == SWT.PAGE_DOWN))) {
847: // We don't want to swallow keyboard navigation keys.
848: return false;
849:
850: } else {
851: Collection match = (InternalPolicy.currentConflicts == null ? null
852: : (Collection) InternalPolicy.currentConflicts
853: .get(sequenceAfterKeyStroke));
854: if (match != null) {
855: errorSequence = sequenceAfterKeyStroke;
856: errorMatch = match;
857: }
858: }
859: }
860:
861: resetState(true);
862: if (sequenceBeforeKeyStroke.isEmpty() && errorSequence != null) {
863: openKeyAssistShell(errorMatch);
864: }
865: return !sequenceBeforeKeyStroke.isEmpty();
866: }
867:
868: /**
869: * <p>
870: * Actually performs the processing of the key event by interacting with the
871: * <code>ICommandManager</code>. If work is carried out, then the event
872: * is stopped here (i.e., <code>event.doit = false</code>). It does not
873: * do any processing if there are no matching key strokes.
874: * </p>
875: * <p>
876: * If the active <code>Shell</code> is not the same as the one to which
877: * the state is associated, then a reset occurs.
878: * </p>
879: *
880: * @param keyStrokes
881: * The set of all possible matching key strokes; must not be
882: * <code>null</code>.
883: * @param event
884: * The event to process; must not be <code>null</code>.
885: */
886: void processKeyEvent(List keyStrokes, Event event) {
887: // Dispatch the keyboard shortcut, if any.
888: boolean eatKey = false;
889: if (!keyStrokes.isEmpty()) {
890: eatKey = press(keyStrokes, event);
891: }
892:
893: if (eatKey) {
894: switch (event.type) {
895: case SWT.KeyDown:
896: event.doit = false;
897: break;
898: case SWT.Traverse:
899: event.detail = SWT.TRAVERSE_NONE;
900: event.doit = true;
901: break;
902: default:
903: }
904: event.type = SWT.NONE;
905: }
906: }
907:
908: /**
909: * Resets the state, and cancels any running timers. If there is a
910: * <code>Shell</code> currently open, then it closes it.
911: *
912: * @param clearRememberedState
913: * Whether the remembered state (dialog bounds) of the key assist
914: * should be forgotten immediately as well.
915: */
916: private final void resetState(final boolean clearRememberedState) {
917: startTime = Long.MAX_VALUE;
918: state.reset();
919: closeMultiKeyAssistShell();
920: if ((keyAssistDialog != null) && clearRememberedState) {
921: keyAssistDialog.clearRememberedState();
922: }
923: }
924: }
|